【问题标题】:Is there a way to check if int is legal enum in C#?有没有办法检查 int 在 C# 中是否是合法的枚举?
【发布时间】:2011-02-10 02:38:51
【问题描述】:

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

【问题讨论】:

标签: c# enums


【解决方案1】:

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

【讨论】:

  • @matti:将“78”转换为LoggingLevel用作存储的任何数字表示形式,然后将其呈现为LoggingLevel枚举值。
  • 似乎IsDefined 不适用于按位枚举成员。
【解决方案2】:

使用Enum.IsDefined

【讨论】:

    【解决方案3】:

    用途:

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

    【讨论】:

      【解决方案4】:

      标准答案是Enum.IsDefined,但这是a:如果在紧密循环中使用会有点慢,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");
      }
      

      【讨论】:

      • 不熟悉 [Flags] 枚举和性能不是问题,因此您的回答似乎是最初发明枚举的原因;)查看您的“点”或其他名称所以你必须有一个观点。打赌你没有得到它们,但考虑一下读取配置文件的情况,其中一个枚举定义中有 257 个值。更不用说其他几十个枚举了。会有很多案例行...
      • @matti - 这听起来是一个极端的例子;无论如何,反序列化是一个专业领域 - 大多数序列化引擎都免费提供枚举验证。
      • @matti - 附注;我会说根据他们的个人优点来对待答案。我有时会完全搞错,“rep 17”的人同样可以给出一个完美的答案。
      • 切换答案很快,但不是通用的。
      【解决方案5】:

      上述解决方案不处理[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 != '-';
      }
      

      可以称其为“黑客”,但其优势在于,通过依赖 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());
          }
      }
      

      【讨论】:

      • 感谢您,我在处理有效标志组合时遇到了类似的问题。作为检查枚举的第一个字符的替代方法,您还可以尝试 int.TryParse(enumValue.ToString())... 如果失败,则您有一组有效的标志。不过,这实际上可能比您的解决方案慢。
      • 此实现无法正确验证负值,因为检查的是非数字字符
      • 好收获!!我会更新我的答案以适应这种情况,谢谢@ChaseMedallion
      • 我最喜欢这个解决方案,提出的数学技巧只有在 [Flags] 具有合理的整数值时才有效。
      【解决方案6】:

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

      enum NetworkStatus{
        Unknown=0,
        Active,
        Slow
      }
      
      int statusCode=2;
      NetworkStatus netStatus = (NetworkStatus) statusCode;
      bool isDefined = netStatus.ToString() != statusCode.ToString();
      

      未针对任何极端情况进行测试。

      【讨论】:

        【解决方案7】:

        为了处理[Flags]也可以使用this solution from 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);
            }
        }
        

        【讨论】:

          【解决方案8】:

          正如其他人所说,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;
          }
          

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

          【讨论】:

          • 至少在 .Net 4.8 'Type.CustomAttributes' 中可用。使用 enumType.CustomAttributes 而不是再次查询它们。
          • CustomAttributes 添加的时候被认为比较慢,添加是因为 WinRT 不支持GetCustomAttributes()。我不确定这些信息的重要性。
          【解决方案9】:

          我知道这是一个老问题,但我今天遇到了这个问题,我想扩展 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. 枚举成员必须以字母字符开头。具体来说,我知道您可以使用下划线作为第一个字符。即,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://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/identifier-names)。我的解决方案在这方面仍然通过。我使用以下枚举进行了测试: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 分钟。
          • GetValues HashSet 每次运行 20 亿次大约需要 50 秒。
          • 每次运行 20 亿次时,IsDefined 大约需要 5 分钟。

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

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

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

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-03-25
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多