有没有办法检查 int 是否是 C# 中的合法枚举?

发布于 2024-08-29 04:57:34 字数 432 浏览 4 评论 0原文

我读过一些 SO 帖子,似乎缺少最基本的操作。

public enum LoggingLevel
{
    Off = 0,
    Error = 1,
    Warning = 2,
    Info = 3,
    Debug = 4,
    Trace = 5
};

if (s == "LogLevel")
{
    _log.LogLevel = (LoggingLevel)Convert.ToInt32("78");
    _log.LogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), "78");
    _log.WriteDebug(_log.LogLevel.ToString());
}

这不会导致异常,它很乐意存储 78。有没有办法验证进入枚举的值?

I've read a few SO posts and it seems most basic operation is missing.

public enum LoggingLevel
{
    Off = 0,
    Error = 1,
    Warning = 2,
    Info = 3,
    Debug = 4,
    Trace = 5
};

if (s == "LogLevel")
{
    _log.LogLevel = (LoggingLevel)Convert.ToInt32("78");
    _log.LogLevel = (LoggingLevel)Enum.Parse(typeof(LoggingLevel), "78");
    _log.WriteDebug(_log.LogLevel.ToString());
}

This causes no exceptions, it's happy to store 78. Is there a way to validate a value going into an enum?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

稀香 2024-09-05 04:57:35

上述解决方案不处理[Flags]情况。

我下面的解决方案可能存在一些性能问题(我确信可以通过各种方式进行优化),但本质上它将始终证明枚举值是否有效

它依赖于三个假设:

  • C# 中的枚举值仅允许为 int,绝对不能为其他
  • C# 中的枚举名称必须以字母字符开头
  • 不能使用有效的枚举名称带减号:-

如果没有匹配的枚举(标志或不),则对枚举调用 ToString() 会返回 int 值。如果匹配允许的枚举值,它将打印匹配的名称。

因此:

[Flags]
enum WithFlags
{
    First = 1,
    Second = 2,
    Third = 4,
    Fourth = 8
}

((WithFlags)2).ToString() ==> "Second"
((WithFlags)(2 + 4)).ToString() ==> "Second, Third"
((WithFlags)20).ToString() ==> "20"

考虑到这两条规则,我们可以假设,如果 .NET Framework 正确完成其工作,则对有效枚举的 ToString() 方法的任何调用都将产生以字母字符作为其值的内容。第一个字符:

public static bool IsValid<TEnum>(this TEnum enumValue)
    where TEnum : struct
{
    var firstChar = enumValue.ToString()[0];
    return (firstChar < '0' || firstChar > '9') && firstChar != '-';
}

人们可以称其为“hack”,但优点是,通过依赖 Microsoft 自己的 Enum 和 C# 标准实现,您不必依赖自己的可能有错误的代码或检查。在性能不是特别关键的情况下,这将节省大量令人讨厌的 switch 语句或其他检查!

编辑

感谢@ChaseMedallion 指出我最初的实现不支持负值。此问题已得到纠正并提供了测试。

以及支持它的测试:

[TestClass]
public class EnumExtensionsTests
{
    [Flags]
    enum WithFlags
    {
        First = 1,
        Second = 2,
        Third = 4,
        Fourth = 8
    }

    enum WithoutFlags
    {
        First = 1,
        Second = 22,
        Third = 55,
        Fourth = 13,
        Fifth = 127
    }

    enum WithoutNumbers
    {
        First, // 1
        Second, // 2
        Third, // 3
        Fourth // 4
    }

    enum WithoutFirstNumberAssigned
    {
        First = 7,
        Second, // 8
        Third, // 9
        Fourth // 10
    }


    enum WithNagativeNumbers
    {
        First = -7,
        Second = -8,
        Third = -9,
        Fourth = -10
    }

    [TestMethod]
    public void IsValidEnumTests()
    {
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4 | 2)).IsValid());
        Assert.IsTrue(((WithFlags)(2)).IsValid());
        Assert.IsTrue(((WithFlags)(3)).IsValid());
        Assert.IsTrue(((WithFlags)(1 + 2 + 4 + 8)).IsValid());

        Assert.IsFalse(((WithFlags)(16)).IsValid());
        Assert.IsFalse(((WithFlags)(17)).IsValid());
        Assert.IsFalse(((WithFlags)(18)).IsValid());
        Assert.IsFalse(((WithFlags)(0)).IsValid());

        Assert.IsTrue(((WithoutFlags)1).IsValid());
        Assert.IsTrue(((WithoutFlags)22).IsValid());
        Assert.IsTrue(((WithoutFlags)(53 | 6)).IsValid());   // Will end up being Third
        Assert.IsTrue(((WithoutFlags)(22 | 25 | 99)).IsValid()); // Will end up being Fifth
        Assert.IsTrue(((WithoutFlags)55).IsValid());
        Assert.IsTrue(((WithoutFlags)127).IsValid());

        Assert.IsFalse(((WithoutFlags)48).IsValid());
        Assert.IsFalse(((WithoutFlags)50).IsValid());
        Assert.IsFalse(((WithoutFlags)(1 | 22)).IsValid());
        Assert.IsFalse(((WithoutFlags)(9 | 27 | 4)).IsValid());

        Assert.IsTrue(((WithoutNumbers)0).IsValid());
        Assert.IsTrue(((WithoutNumbers)1).IsValid());
        Assert.IsTrue(((WithoutNumbers)2).IsValid());
        Assert.IsTrue(((WithoutNumbers)3).IsValid());
        Assert.IsTrue(((WithoutNumbers)(1 | 2)).IsValid()); // Will end up being Third
        Assert.IsTrue(((WithoutNumbers)(1 + 2)).IsValid()); // Will end up being Third

        Assert.IsFalse(((WithoutNumbers)4).IsValid());
        Assert.IsFalse(((WithoutNumbers)5).IsValid());
        Assert.IsFalse(((WithoutNumbers)25).IsValid());
        Assert.IsFalse(((WithoutNumbers)(1 + 2 + 3)).IsValid());

        Assert.IsTrue(((WithoutFirstNumberAssigned)7).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)8).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)9).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)10).IsValid());

        Assert.IsFalse(((WithoutFirstNumberAssigned)11).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)6).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(7 | 9)).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(8 + 10)).IsValid());

        Assert.IsTrue(((WithNagativeNumbers)(-7)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-8)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-9)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-10)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(-11)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(7)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(8)).IsValid());
    }
}

The above solutions do not deal with [Flags] situations.

My solution below may have some performance issues (I'm sure one could optimise in various ways) but essentially it will always prove whether an enum value is valid or not.

It relies on three assumptions:

  • Enum values in C# are only allowed to be int, absolutely nothing else
  • Enum names in C# must begin with an alphabetic character
  • No valid enum name can being with a minus sign: -

Calling ToString() on an enum returns either the int value if no enum (flag or not) is matched. If an allowed enum value is matched, it will print the name of the match(es).

So:

[Flags]
enum WithFlags
{
    First = 1,
    Second = 2,
    Third = 4,
    Fourth = 8
}

((WithFlags)2).ToString() ==> "Second"
((WithFlags)(2 + 4)).ToString() ==> "Second, Third"
((WithFlags)20).ToString() ==> "20"

With these two rules in mind we can assume that if the .NET Framework does its job correctly that any calls to a valid enum's ToString() method will result in something that has an alphabetic character as its first character:

public static bool IsValid<TEnum>(this TEnum enumValue)
    where TEnum : struct
{
    var firstChar = enumValue.ToString()[0];
    return (firstChar < '0' || firstChar > '9') && firstChar != '-';
}

One could call it a "hack", but the advantages are that by relying on Microsoft's own implementation of Enum and C# standards, you're not relying on your own potentially buggy code or checks. In situations where performance is not exceptionally critical, this will save a lot of nasty switch statements or other checks!

Edit

Thanks to @ChaseMedallion for pointing out that my original implementation did not support negative values. This has been remedied and tests provided.

And the tests to back it up:

[TestClass]
public class EnumExtensionsTests
{
    [Flags]
    enum WithFlags
    {
        First = 1,
        Second = 2,
        Third = 4,
        Fourth = 8
    }

    enum WithoutFlags
    {
        First = 1,
        Second = 22,
        Third = 55,
        Fourth = 13,
        Fifth = 127
    }

    enum WithoutNumbers
    {
        First, // 1
        Second, // 2
        Third, // 3
        Fourth // 4
    }

    enum WithoutFirstNumberAssigned
    {
        First = 7,
        Second, // 8
        Third, // 9
        Fourth // 10
    }


    enum WithNagativeNumbers
    {
        First = -7,
        Second = -8,
        Third = -9,
        Fourth = -10
    }

    [TestMethod]
    public void IsValidEnumTests()
    {
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4)).IsValid());
        Assert.IsTrue(((WithFlags)(1 | 4 | 2)).IsValid());
        Assert.IsTrue(((WithFlags)(2)).IsValid());
        Assert.IsTrue(((WithFlags)(3)).IsValid());
        Assert.IsTrue(((WithFlags)(1 + 2 + 4 + 8)).IsValid());

        Assert.IsFalse(((WithFlags)(16)).IsValid());
        Assert.IsFalse(((WithFlags)(17)).IsValid());
        Assert.IsFalse(((WithFlags)(18)).IsValid());
        Assert.IsFalse(((WithFlags)(0)).IsValid());

        Assert.IsTrue(((WithoutFlags)1).IsValid());
        Assert.IsTrue(((WithoutFlags)22).IsValid());
        Assert.IsTrue(((WithoutFlags)(53 | 6)).IsValid());   // Will end up being Third
        Assert.IsTrue(((WithoutFlags)(22 | 25 | 99)).IsValid()); // Will end up being Fifth
        Assert.IsTrue(((WithoutFlags)55).IsValid());
        Assert.IsTrue(((WithoutFlags)127).IsValid());

        Assert.IsFalse(((WithoutFlags)48).IsValid());
        Assert.IsFalse(((WithoutFlags)50).IsValid());
        Assert.IsFalse(((WithoutFlags)(1 | 22)).IsValid());
        Assert.IsFalse(((WithoutFlags)(9 | 27 | 4)).IsValid());

        Assert.IsTrue(((WithoutNumbers)0).IsValid());
        Assert.IsTrue(((WithoutNumbers)1).IsValid());
        Assert.IsTrue(((WithoutNumbers)2).IsValid());
        Assert.IsTrue(((WithoutNumbers)3).IsValid());
        Assert.IsTrue(((WithoutNumbers)(1 | 2)).IsValid()); // Will end up being Third
        Assert.IsTrue(((WithoutNumbers)(1 + 2)).IsValid()); // Will end up being Third

        Assert.IsFalse(((WithoutNumbers)4).IsValid());
        Assert.IsFalse(((WithoutNumbers)5).IsValid());
        Assert.IsFalse(((WithoutNumbers)25).IsValid());
        Assert.IsFalse(((WithoutNumbers)(1 + 2 + 3)).IsValid());

        Assert.IsTrue(((WithoutFirstNumberAssigned)7).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)8).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)9).IsValid());
        Assert.IsTrue(((WithoutFirstNumberAssigned)10).IsValid());

        Assert.IsFalse(((WithoutFirstNumberAssigned)11).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)6).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(7 | 9)).IsValid());
        Assert.IsFalse(((WithoutFirstNumberAssigned)(8 + 10)).IsValid());

        Assert.IsTrue(((WithNagativeNumbers)(-7)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-8)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-9)).IsValid());
        Assert.IsTrue(((WithNagativeNumbers)(-10)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(-11)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(7)).IsValid());
        Assert.IsFalse(((WithNagativeNumbers)(8)).IsValid());
    }
}
北斗星光 2024-09-05 04:57:35

规范的答案是 Enum.IsDefined,但这 a:如果在紧密循环中使用,有点慢,b:对于 [Flags] 枚举没有用。

就我个人而言,我不会再担心这一点,只需适当地切换,记住:

  • 如果可以不识别所有内容(并且不执行任何操作),那么就不要添加默认值: (或者有一个空的 default: 解释原因)
  • 如果存在合理的默认行为,请将其放入 default:
  • 否则,处理您知道的默认行为about 并为其余部分抛出异常:

像这样:

switch(someflag) {
    case TriBool.Yes:
        DoSomething();
        break;
    case TriBool.No:
        DoSomethingElse();
        break;
    case TriBool.FileNotFound:
        DoSomethingOther();
        break;
    default:
        throw new ArgumentOutOfRangeException("someflag");
}

The canonical answer would be Enum.IsDefined, but that is a: a bit slow if used in a tight loop, and b: not useful for [Flags] enums.

Personally, I'd stop worrying about that, and just switch appropriately, remembering:

  • if it is OK not to recognise everything (and just not do anything), then don't add a default: (or have an empty default: explaining why)
  • if there is a sensible default behaviour, put that in the default:
  • otherwise, handle the ones you know about and throw an exception for the rest:

Like so:

switch(someflag) {
    case TriBool.Yes:
        DoSomething();
        break;
    case TriBool.No:
        DoSomethingElse();
        break;
    case TriBool.FileNotFound:
        DoSomethingOther();
        break;
    default:
        throw new ArgumentOutOfRangeException("someflag");
}
眼前雾蒙蒙 2024-09-05 04:57:35

使用:

Enum.IsDefined ( typeof ( Enum ), EnumValue );

Use:

Enum.IsDefined ( typeof ( Enum ), EnumValue );
余生再见 2024-09-05 04:57:35

为了处理 [Flags] 您还可以使用 C# Cookbook 中的此解决方案

首先,向枚举添加一个新的 ALL 值:

[Flags]
enum Language
{
    CSharp = 1, VBNET = 2, VB6 = 4, 
    All = (CSharp | VBNET | VB6)
}

然后,检查该值是否在 ALL 中:

public bool HandleFlagsEnum(Language language)
{
    if ((language & Language.All) == language)
    {
        return (true);
    }
    else
    {
        return (false);
    }
}

In order to deal with [Flags] you can also use this solution from C# Cookbook:

First, add a new ALL value to your enum:

[Flags]
enum Language
{
    CSharp = 1, VBNET = 2, VB6 = 4, 
    All = (CSharp | VBNET | VB6)
}

Then, check if the value is in ALL:

public bool HandleFlagsEnum(Language language)
{
    if ((language & Language.All) == language)
    {
        return (true);
    }
    else
    {
        return (false);
    }
}
放肆 2024-09-05 04:57:35

正如其他人所说,即使您具有使用 FlagsAttribute 装饰的枚举的有效位标志组合,Enum.IsDefined 也会返回 false

遗憾的是,创建对 有效 位标志返回 true 的方法的唯一方法有点冗长:

public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    // For enums decorated with the FlagsAttribute, allow sets of flags.
    if (!valid && enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true)
    {
        long mask = 0;
        foreach (object definedValue in Enum.GetValues(enumType))
            mask |= Convert.ToInt64(definedValue);
        long longValue = Convert.ToInt64(value);
        valid = (mask & longValue) == longValue;
    }
    return valid;
}

您可能希望将 GetCustomAttribute 的结果缓存在字典中:

private static readonly Dictionary<Type, bool> _flagEnums = new Dictionary<Type, bool>();
public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    if (!valid)
    {
        // For enums decorated with the FlagsAttribute, allow sets of flags.
        if (!_flagEnums.TryGetValue(enumType, out bool isFlag))
        {
            isFlag = enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true;
            _flagEnums.Add(enumType, isFlag);
        }
        if (isFlag)
        {
            long mask = 0;
            foreach (object definedValue in Enum.GetValues(enumType))
                mask |= Convert.ToInt64(definedValue);
            long longValue = Convert.ToInt64(value);
            valid = (mask & longValue) == longValue;
        }
    }
    return valid;
}

请注意上面的代码对 T 使用了新的 Enum 约束,该约束仅自 C# 7.3 起可用。在旧版本中,您需要传递一个对象值并对其调用GetType()

As the others said, Enum.IsDefined returns false even if you have a valid combination of bit flags for an enum decorated with the FlagsAttribute.

Sadly, the only way to create a method returning true for valid bit flags is a bit lengthy:

public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    // For enums decorated with the FlagsAttribute, allow sets of flags.
    if (!valid && enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true)
    {
        long mask = 0;
        foreach (object definedValue in Enum.GetValues(enumType))
            mask |= Convert.ToInt64(definedValue);
        long longValue = Convert.ToInt64(value);
        valid = (mask & longValue) == longValue;
    }
    return valid;
}

You may want to cache the results of GetCustomAttribute in a dictionary:

private static readonly Dictionary<Type, bool> _flagEnums = new Dictionary<Type, bool>();
public static bool ValidateEnumValue<T>(T value) where T : Enum
{
    // Check if a simple value is defined in the enum.
    Type enumType = typeof(T);
    bool valid = Enum.IsDefined(enumType, value);
    if (!valid)
    {
        // For enums decorated with the FlagsAttribute, allow sets of flags.
        if (!_flagEnums.TryGetValue(enumType, out bool isFlag))
        {
            isFlag = enumType.GetCustomAttributes(typeof(FlagsAttribute), false)?.Any() == true;
            _flagEnums.Add(enumType, isFlag);
        }
        if (isFlag)
        {
            long mask = 0;
            foreach (object definedValue in Enum.GetValues(enumType))
                mask |= Convert.ToInt64(definedValue);
            long longValue = Convert.ToInt64(value);
            valid = (mask & longValue) == longValue;
        }
    }
    return valid;
}

Note that the code above uses the new Enum constraint on T which is only available since C# 7.3. You need to pass an object value in older versions and call GetType() on it.

一个人的夜不怕黑 2024-09-05 04:57:35

一种方法是依靠强制转换和枚举到字符串的转换。将 int 转换为 Enum 类型时,int 会转换为相应的枚举值,或者如果未为 int 定义枚举值,则生成的枚举仅包含 int 作为值。

enum NetworkStatus{
  Unknown=0,
  Active,
  Slow
}

int statusCode=2;
NetworkStatus netStatus = (NetworkStatus) statusCode;
bool isDefined = netStatus.ToString() != statusCode.ToString();

未测试任何边缘情况。

One way to do would be to rely on casting and enum to string conversion. When casting int to an Enum type the int is either converted to a corresponding enum value or the resulting enum just contains int as a value if enum value is not defined for the int.

enum NetworkStatus{
  Unknown=0,
  Active,
  Slow
}

int statusCode=2;
NetworkStatus netStatus = (NetworkStatus) statusCode;
bool isDefined = netStatus.ToString() != statusCode.ToString();

Not tested for any edge cases.

肤浅与狂妄 2024-09-05 04:57:35

我知道这是一个老问题,但我今天遇到了这个问题,我想扩展 Josh Comley 的答案(https://stackoverflow .com/a/23177585/3403999

我想解决乔什的回答中有几个错误的假设:

  1. 它假设“-”始终是负号。我不知道是否有任何文化使用不同的符号,但 .Net 当然允许在 NumberFormatInfo 中使用它(https://learn.microsoft.com/en-us/dotnet/api/system.globalization.numberformatinfo. Negativesign?view=net -5.0)。我能想到的唯一可能常见的是括号,即 (1) == -1。
  2. 枚举成员必须以字母字符开头。具体来说,我知道您可以使用下划线作为第一个字符。 IE,enum MyEnum { _One = 1 } 有效。
  3. 不太确定这完全错误,但它假设“0”到“9”和“-”范围之外的任何内容都是有效的字母字符。这似乎是一个错误的假设,因为该范围之外的控制字符会返回 true - 尽管我认为您无法将这些控制字符放入枚举成员名称中而不引发编译错误。

无论如何,这是我更新的解决方案:

public static bool IsValid<TEnum>(this TEnum value) where TEnum : System.Enum
{
    char first = value.ToString()[0];
    return (char.IsLetter(first) || first == '_');
}

我确实发现您可以在枚举成员名称中使用其他语言的 Unicode 字母(https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/identifier-names< /a>)。我的解决方案在这方面仍然通过。我使用以下枚举进行了测试:enum MyEnum { \u05D0 }。枚举已编译,IsValid 返回 true。

我很好奇,与使用带有填充 Enum.GetValues(typeof(TEnum)) 的 HashSet 的静态帮助程序类相比,您会采取这种路线,在其中检查是否会受到什么样的性能影响HashSet 包含枚举值。我们的想法是,Enum.GetValues 和 Enum.IsDefined 都只是昂贵的反射命中的包装,因此您可以使用 GetValues 执行一次反射,缓存结果,然后只需检查 HashSet。

我使用 StopWatch 和 Random 进行了一个相当简单的测试,它将生成有效的 &无效的枚举值,然后我通过 3 种不同的方法运行它们:ToString 方法、GetValues HashSet 方法和 IsDefined 方法。我让他们对每个方法执行 int.MaxValue 次。结果:

  • 每次运行 20 亿次,ToString 平均需要 2 分钟左右。
  • GetValues HashSet 每次运行 20 亿次大约需要 50 秒。
  • 每次运行 20 亿次 IsDefined 大约需要 5 分钟。

因此,如果性能是一个问题,或者您正在执行循环,那么所有推荐 IsDefined 的解决方案可能都是一个坏主意。如果您只是使用它以某种方式验证单个实例上的用户输入,那么它可能并不重要。

对于 HashSet,对于您运行的每个不同的枚举来说,性能都会受到影响(因为第一次运行新的枚举类型会生成一个新的静态 HashSet)。不科学,但在我的 PC 上,在开始使用 ToString 方法执行单个枚举之前,我的盈亏平衡点似乎约为 200k 到 300k 运行。

ToString 方法虽然不是最快的方法,但具有处理 IsDefined 和 HashSet 都无法容纳的 Flags 枚举的额外好处。

如果性能确实是一个问题,请不要使用这 3 种方法中的任何一种。相反,编写一个方法来验证针对该枚举优化的特定枚举。

另请注意,我的测试使用相对较小的枚举(5 个左右的元素)。一旦你开始使用更大的枚举,我不知道 ToString 与 HashSet 之间的性能如何。

I know this is an old question, but I ran into this today, and I wanted to expand on Josh Comley's answer (https://stackoverflow.com/a/23177585/3403999)

There's a couple of wrong assumptions in Josh's answer that I wanted to address:

  1. It assumes that the '-' is always the negative sign. I don't know if there is any cultures that use a different sign, but .Net certainly allows for it in the NumberFormatInfo (https://learn.microsoft.com/en-us/dotnet/api/system.globalization.numberformatinfo.negativesign?view=net-5.0). About the only one I can think of that might be common is the parenthesis, ie (1) == -1.
  2. Enum members have to start with an alphabetic character. Specifically, I know you can use an underscore as the first char. IE, enum MyEnum { _One = 1 } is valid.
  3. Not really sure this exactly wrong, but it made the assumption that anything outside the range of '0' to '9' and '-' is a valid alphabetic character. It seemed like a bad assumption cause there are control characters outside that range that would return true - albeit, I don't think you can get those control characters into an enum member name without it throwing a compile error.

Anyway, here's my updated solution:

public static bool IsValid<TEnum>(this TEnum value) where TEnum : System.Enum
{
    char first = value.ToString()[0];
    return (char.IsLetter(first) || first == '_');
}

I did discover that you can use Unicode letters from other languages in enum member names (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/identifier-names). My solution still passes in this regard. I tested with the following enum: enum MyEnum { \u05D0 }. The enum compiled, and the IsValid returned true.

I was curious what kind of performance hit you'd take going this route vs using a static helper class with a HashSet that is filled with Enum.GetValues(typeof(TEnum)) where you check to see if the HashSet contains the enum value. The thought being that both Enum.GetValues and Enum.IsDefined are just wrappers around expensive Reflection hits, so you do the Reflection once with GetValues, cache the results, and then just check the HashSet going forward.

I ran a fairly simple test with a StopWatch and Random that would generate valid & invalid enum values, and then I ran them through 3 different methods: the ToString method, the GetValues HashSet method, and the IsDefined method. I had them do each method int.MaxValue times. The results:

  • ToString averaged about 2 minutes every time I ran it 2 billion times.
  • GetValues HashSet about 50 seconds every time I ran it 2 billion times.
  • IsDefined about 5 minutes every time I ran it 2 billion times.

So all the solutions recommending IsDefined are probably a bad idea if performance is a concern, or your doing a loop. If you are only using it somehow validate user input on single instances, it probably doesn't matter.

For the HashSet, it's a small performance hit for each different enum you run through it (cause the first time a new enum type gets ran through generates a new static HashSet). Not scientific, but it seemed my break even point on my PC was about 200k to 300k runs for a single enum before it started out performing using the ToString method.

The ToString method, while not the fastest had the added benefit of handling Flags enums that neither the IsDefined nor HashSet accommodate.

If performance really is a concern, don't use any of these 3 methods. Instead write a method that validates on a specific enum optimized to that enum.

Also note that my tests were with relatively small enums (5 or so elements). I don't know how performance between ToString vs HashSet once you start getting into larger enums.

谁对谁错谁最难过 2024-09-05 04:57:35

对一个老问题的另一个答案,源于以下事实:a)如果您有一个 int 对于您的枚举来说不是有效值,并且您尝试将其转换为该枚举类型,它将无论如何强制转换它,但结果值将与您定义的任何枚举值都不匹配,并且 b) 有一个通用版本的 Enum.IsDefined ,它将根据您传递给它的内容推断出类型。这是一个利用上述内容的通用 TryGetEnumValue 示例。

public static bool TryGetEnumValue<TEnum>(int tryMe, out TEnum enumValue) where TEnum : struct, Enum
{
    // This will cast the int to a TEnum, even if it is not a defined TEnum value.
    var val = (TEnum)(object)tryMe;

    // So now we test to see if it is a defined T enum value.
    if (Enum.IsDefined(val))
    {
        enumValue = val;
        return true;
    }
    else
    {
        enumValue = default;
        return false;
    }
}

一些测试显示了它的工作效果:

public enum PorridgeNiceness
{
    NotSet,
    TooCold,
    JustRight,
    TooHot
}
// Having not explictly assigned int values to my enum above, 
// they will be 0, 1, 2, 3 in that order.

[TestMethod] 
public void Test_() 
{
    Assert.IsTrue(TryGetEnumValue(1, out PorridgeNiceness a));
    Assert.AreEqual(PorridgeNiceness.TooCold, a);

    Assert.IsTrue(TryGetEnumValue(2, out PorridgeNiceness b));
    Assert.AreEqual(PorridgeNiceness.JustRight, b);

    Assert.IsFalse(TryGetEnumValue(99, out PorridgeNiceness _));
}

根据本线程中的其他讨论,此方法不会识别复合 Flags 值。

Another answer to an old question, arising out of the facts that a) if you have an int that is not a valid value for your enum, and you try to cast it to that enum type, it will cast it anyway but the resulting value will not match any of your defined enum values, and b) there is a generic version of Enum.IsDefined which will infer the type from what you pass to it. Here's a generic TryGetEnumValue example making use of the above.

public static bool TryGetEnumValue<TEnum>(int tryMe, out TEnum enumValue) where TEnum : struct, Enum
{
    // This will cast the int to a TEnum, even if it is not a defined TEnum value.
    var val = (TEnum)(object)tryMe;

    // So now we test to see if it is a defined T enum value.
    if (Enum.IsDefined(val))
    {
        enumValue = val;
        return true;
    }
    else
    {
        enumValue = default;
        return false;
    }
}

And some tests showing it at work:

public enum PorridgeNiceness
{
    NotSet,
    TooCold,
    JustRight,
    TooHot
}
// Having not explictly assigned int values to my enum above, 
// they will be 0, 1, 2, 3 in that order.

[TestMethod] 
public void Test_() 
{
    Assert.IsTrue(TryGetEnumValue(1, out PorridgeNiceness a));
    Assert.AreEqual(PorridgeNiceness.TooCold, a);

    Assert.IsTrue(TryGetEnumValue(2, out PorridgeNiceness b));
    Assert.AreEqual(PorridgeNiceness.JustRight, b);

    Assert.IsFalse(TryGetEnumValue(99, out PorridgeNiceness _));
}

As per other discussions in this thread, this method won't recognise composite Flags values.

往事随风而去 2024-09-05 04:57:34

查看 Enum.IsDefined

用法:

if(Enum.IsDefined(typeof(MyEnum), value))
    MyEnum a = (MyEnum)value; 

这是示例从该页面:

using System;    
[Flags] public enum PetType
{
   None = 0, Dog = 1, Cat = 2, Rodent = 4, Bird = 8, Reptile = 16, Other = 32
};

public class Example
{
   public static void Main()
   {
      object value;     
      // Call IsDefined with underlying integral value of member.
      value = 1;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with invalid underlying integral value.
      value = 64;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with string containing member name.
      value = "Rodent";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with a variable of type PetType.
      value = PetType.Dog;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = PetType.Dog | PetType.Cat;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with uppercase member name.      
      value = "None";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = "NONE";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with combined value
      value = PetType.Dog | PetType.Bird;
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = value.ToString();
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
   }
}

该示例显示以下输出:

//       1: True
//       64: False
//       Rodent: True
//       Dog: True
//       Dog, Cat: False
//       None: True
//       NONE: False
//       9: False
//       Dog, Bird: False

Check out Enum.IsDefined

Usage:

if(Enum.IsDefined(typeof(MyEnum), value))
    MyEnum a = (MyEnum)value; 

This is the example from that page:

using System;    
[Flags] public enum PetType
{
   None = 0, Dog = 1, Cat = 2, Rodent = 4, Bird = 8, Reptile = 16, Other = 32
};

public class Example
{
   public static void Main()
   {
      object value;     
      // Call IsDefined with underlying integral value of member.
      value = 1;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with invalid underlying integral value.
      value = 64;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with string containing member name.
      value = "Rodent";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with a variable of type PetType.
      value = PetType.Dog;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = PetType.Dog | PetType.Cat;
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with uppercase member name.      
      value = "None";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = "NONE";
      Console.WriteLine("{0}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      // Call IsDefined with combined value
      value = PetType.Dog | PetType.Bird;
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
      value = value.ToString();
      Console.WriteLine("{0:D}: {1}", value, Enum.IsDefined(typeof(PetType), value));
   }
}

The example displays the following output:

//       1: True
//       64: False
//       Rodent: True
//       Dog: True
//       Dog, Cat: False
//       None: True
//       NONE: False
//       9: False
//       Dog, Bird: False
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文