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

时间:2010-04-20 11:44:06

标签: c# enums

我已经阅读了一些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。有没有办法验证进入枚举的值?

9 个答案:

答案 0 :(得分:211)

查看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

答案 1 :(得分:27)

以上解决方案不涉及[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 != '-';
}

有人可以称之为&#34; hack&#34;,但优点是依靠微软自己实施的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());
    }
}

答案 2 :(得分:14)

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

就个人而言,我不再担心这一点,而只是恰恰switch,记住:

  • 如果可以不识别所有内容(并且不做任何事情),那么请不要添加default:(或使用空default:解释原因)
  • 如果存在合理的默认行为,请将其放在default:
  • 否则,处理你所知道的并为其余部分抛出异常:

像这样:

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

答案 3 :(得分:6)

使用:

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

答案 4 :(得分:5)

使用Enum.IsDefined

答案 5 :(得分:4)

为了处理ALL,您还可以使用this solution from C# Cookbook

首先,为您的枚举添加新的[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); } }

each

答案 6 :(得分:2)

一种方法是依靠cast和enum来进行字符串转换。当将int转换为Enum类型时,int被转换为相应的枚举值,或者如果没有为int定义枚举值,则生成的枚举仅包含int作为值。

enum NetworkStatus{
  Unknown=0,
  Active,
  Slow
}

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

未针对任何边缘情况进行测试。

答案 7 :(得分:0)

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

遗憾的是,创建为 valid 位标志返回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;
}

请注意,上面的代码对Enum使用了新的T约束,该约束仅从C#7.3起可用。您需要通过较旧版本的object value并在其上调用GetType()

答案 8 :(得分:0)

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

Josh 的回答中有几个错误的假设需要解决:

  1. 它假定“-”始终是负号。我不知道是否有任何文化使用不同的符号,但 .Net 肯定允许在 NumberFormatInfo (https://docs.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 == '_');
}

我确实发现您可以在枚举成员名称 (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/identifier-names) 中使用来自其他语言的 Unicode 字母。我的解决方案在这方面仍然通过。我使用以下枚举进行了测试: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 次。结果:

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

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

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

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

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

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