【问题标题】:Validate Enum Values验证枚举值
【发布时间】:2010-09-06 00:44:04
【问题描述】:

我需要验证一个整数才能知道它是否是一个有效的枚举值。

在 C# 中执行此操作的最佳方法是什么?

【问题讨论】:

  • 对于带有标志的方法,在重复问题上检查此答案可能很有用:stackoverflow.com/a/23177585/5190842
  • 检查EnumDataTypeAttribute
  • 最快的方法是不验证枚举并使用默认行为,任何枚举都应该有“无”或“默认”之类的成员。你可以使用 switch 默认情况。

标签: c# validation enums


【解决方案1】:

你一定会喜欢这些认为数据不仅总是来自 UI,而且在你的控制范围内的 UI 的人!

IsDefined 适用于大多数情况,您可以从以下开始:

public static bool TryParseEnum<TEnum>(this int enumValue, out TEnum retVal)
{
 retVal = default(TEnum);
 bool success = Enum.IsDefined(typeof(TEnum), enumValue);
 if (success)
 {
  retVal = (TEnum)Enum.ToObject(typeof(TEnum), enumValue);
 }
 return success;
}

(如果您认为它不是合适的 int 扩展名,显然只需删除“this”)

【讨论】:

  • 另请注意,Enum.IsDefined 使用可能导致性能问题的反射
  • 已经检查了IsDefined() 为什么不直接将 int 转换为成功条件:retVal = (TEnum)enumValue
  • @jeromej 我在问为什么我们不能在成功条件下使用retVal = (TEnum)enumvalue 而不是retVal = (TEnum)Enum.ToObject(typeof(TEnum), enumValue)。我们已经确认enumValue IsDefined()
  • @MattJenkins 对。我看不懂,我的错。现在删除我的评论。
  • @GuneyOzsan 是的,但这不是范围检查。如果你 100% 确定你有连续的枚举,并且一个简单的 max-check 就可以了,那么就可以使用编程定义的 const (并希望人们记得保持最新)或“最后一个值”类型的方法,即:枚举我的枚举{val1,val2,val3,LAST_VAL=val3}。两者都有其缺点(主要是对于本世纪的语言来说“有点笨拙”),但正如所讨论的,两者都更快。
【解决方案2】:

恕我直言,标记为答案的帖子不正确。
参数和数据验证是我几十年前钻研的事情之一。

为什么

验证是必需的,因为基本上任何整数值都可以分配给枚举而不会引发错误。
我花了很多天研究 C# 枚举验证,因为它在很多情况下都是必要的功能。

在哪里

对我来说,枚举验证的主要目的是验证从文件中读取的数据:您永远不知道文件是否已损坏、是否被外部修改或被故意入侵。
并且对从剪贴板粘贴的应用程序数据进行枚举验证:您永远不知道用户是否编辑了剪贴板内容。

也就是说,我花了几天时间研究和测试许多方法,包括分析我能找到或设计的每种方法的性能。

调用 System.Enum 中的任何内容都非常慢,以至于对于包含成百上千个对象的函数来说,这些对象的属性中具有一个或多个必须验证边界的枚举,这会导致明显的性能损失。

最重要的是,在验证枚举值时远离 System.Enum 类中的所有内容,它非常慢。

结果

我目前用于枚举验证的方法可能会引起这里许多程序员的翻白眼,但恕我直言,对于我的特定应用程序设计而言,它是最不邪恶的。

我定义了一个或两个常量,它们是枚举的上限和(可选)下限,并在一对 if() 语句中使用它们进行验证。
一个缺点是,如果您更改枚举,您必须确保更新常量。
此方法也仅在枚举是“自动”样式时才有效,其中每个枚举元素都是增量整数值,例如 0,1,2,3,4,.... 它不能与标志或枚举一起正常工作具有非增量值。

另请注意,此方法几乎与常规 int32 上的 "" 一样快(在我的测试中获得了 38,000 个滴答声)。

例如:

public const MyEnum MYENUM_MINIMUM = MyEnum.One;
public const MyEnum MYENUM_MAXIMUM = MyEnum.Four;

public enum MyEnum
{
    One,
    Two,
    Three,
    Four
};

public static MyEnum Validate(MyEnum value)
{
    if (value < MYENUM_MINIMUM) { return MYENUM_MINIMUM; }
    if (value > MYENUM_MAXIMUM) { return MYENUM_MAXIMUM; }
    return value;
}

性能

对于那些感兴趣的人,我介绍了枚举验证的以下变体,以下是结果。

分析是在发布编译时以随机整数输入值在每个方法上循环一百万次执行的。每个测试运行 10 次以上并取平均值。滴答结果包括执行的总时间,其中包括随机数生成等,但这些在测试中将保持不变。 1 个滴答 = 10ns。

注意这里的代码不是完整的测试代码,它只是基本的枚举验证方法。还测试了许多其他变体,所有变体的结果都与此处显示的 1,800,000 个滴答声相似。

列出从最慢到最快的四舍五入结果,希望没有错别字。

在方法中确定的界限 = 13,600,000 个滴答声

public static T Clamp<T>(T value)
{
    int minimum = Enum.GetValues(typeof(T)).GetLowerBound(0);
    int maximum = Enum.GetValues(typeof(T)).GetUpperBound(0);

    if (Convert.ToInt32(value) < minimum) { return (T)Enum.ToObject(typeof(T), minimum); }
    if (Convert.ToInt32(value) > maximum) { return (T)Enum.ToObject(typeof(T), maximum); }
    return value;
}

Enum.IsDefined = 1,800,000 滴答
注意:此代码版本不会限制到最小值/最大值,但如果超出范围则返回默认值。

public static T ValidateItem<T>(T eEnumItem)
{
    if (Enum.IsDefined(typeof(T), eEnumItem) == true)
        return eEnumItem;
    else
        return default(T);
}

System.Enum Convert Int32 with casts = 1,800,000 滴答声

public static Enum Clamp(this Enum value, Enum minimum, Enum maximum)
{
    if (Convert.ToInt32(value) < Convert.ToInt32(minimum)) { return minimum; }
    if (Convert.ToInt32(value) > Convert.ToInt32(maximum)) { return maximum; }
    return value;
}

if() 最小/最大常数 = 43,000 个滴答 = 获胜者快 42 倍和 316 倍。

public static MyEnum Clamp(MyEnum value)
{
    if (value < MYENUM_MINIMUM) { return MYENUM_MINIMUM; }
    if (value > MYENUM_MAXIMUM) { return MYENUM_MAXIMUM; }
    return value;
}

-eol-

【讨论】:

  • 我们的大量枚举不是连续的,因此在许多情况下不是一个选项。 'Enum.IsDefined(typeof(T)' 在您的测试场景中会很慢,因为 .net 正在执行大量反射、装箱等操作。每次解析导入文件中的一行时调用它永远不会很快。如果性能是关键,然后我会考虑在导入开始时调用 Enum.GetValues。它永远不会像简单的 比较那样快,但你知道它适用于所有枚举。或者你可以有一个更智能的枚举解析器,我会发布另一个答案,因为没有空间来整齐地回复!
  • @johnny 5 - 根据上面的信息:此方法也仅适用于枚举是“自动”样式,其中每个枚举元素都是增量整数值,例如 0、1、2、3, 4,.... 它不能与具有非增量值的标志或枚举一起正常工作。
  • @Vman - “每次解析导入文件中的一行时调用它永远不会很快。” - 它将明显快于驱动器的读取速度。
  • 也许其他一些进程可能想要一块硬盘驱动器操作?千刀万剐。
  • 过早优化。绝大多数情况下不会从文件中处理足够数量的枚举,从而对性能产生任何重大影响。测试一下,如果有性能问题,再测试并选择最好的处理方法。
【解决方案3】:

正如其他人所提到的,Enum.IsDefined 很慢,如果它处于循环中,您必须注意这一点。

进行多重比较时,更快的方法是首先将值放入HashSet。然后只需使用Contains 来检查该值是否有效,如下所示:

int userInput = 4;
// below, Enum.GetValues converts enum to array. We then convert the array to hashset.
HashSet<int> validVals = new HashSet<int>((int[])Enum.GetValues(typeof(MyEnum)));
// the following could be in a loop, or do multiple comparisons, etc.
if (validVals.Contains(userInput))
{
    // is valid
}

【讨论】:

  • 对此答案的另一种看法是使用HashSet&lt;MyEnum&gt;,如果您验证的是enum 而不是int,则无需强制转换为int。设置它的代码如下所示:var validVals = new HashSet&lt;MyEnum&gt;(Enum.GetValues(typeof(MyEnum)).Cast&lt;MyEnum&gt;());。用法当然是一样的。
  • 碰巧 HashSet 由于某种原因比 HashSet 快得多,因此在进行检查之前强制转换为 int 会大大加快速度。
  • 但请注意GetValuesnew HashSet 的额外分配。在热路径中,这会降低吞吐量。
【解决方案4】:

这是一个快速的通用解决方案,使用静态构造的HashSet&lt;T&gt;

您可以在工具箱中定义一次,然后将其用于所有枚举验证。

public static class EnumHelpers
{
    /// <summary>
    /// Returns whether the given enum value is a defined value for its type.
    /// Throws if the type parameter is not an enum type.
    /// </summary>
    public static bool IsDefined<T>(T enumValue)
    {
        if (typeof(T).BaseType != typeof(System.Enum)) throw new ArgumentException($"{nameof(T)} must be an enum type.");

        return EnumValueCache<T>.DefinedValues.Contains(enumValue);
    }

    /// <summary>
    /// Statically caches each defined value for each enum type for which this class is accessed.
    /// Uses the fact that static things exist separately for each distinct type parameter.
    /// </summary>
    internal static class EnumValueCache<T>
    {
        public static HashSet<T> DefinedValues { get; }

        static EnumValueCache()
        {
            if (typeof(T).BaseType != typeof(System.Enum)) throw new Exception($"{nameof(T)} must be an enum type.");

            DefinedValues = new HashSet<T>((T[])System.Enum.GetValues(typeof(T)));
        }
    }
}

请注意,通过使用带有字符串键的字典(注意不区分大小写和数字字符串表示),这种方法也很容易扩展到枚举解析。

【讨论】:

    【解决方案5】:

    Brad Abrams 在他的帖子 The Danger of Oversimplification 中特别警告了 Enum.IsDefined

    摆脱此要求(即验证枚举的需要)的最佳方法是删除用户可能会出错的方式,例如某种输入框。例如,使用带有下拉菜单的枚举来强制使用有效的枚举。

    【讨论】:

    • 仅将枚举放入下拉框中的建议适用于 WinForms,但不适用于需要验证恶意输入的 WebForms。
    • 我的输入来自一个 XML 文件,该文件由许多可能的程序之一生成,我无法控制数据质量差异很大的地方。 Enum.IsDefined 真的那么糟糕吗,因为在这种情况下它似乎最适合我?
    • @Richard 与生活中的任何事情一样,在某些特定情况下,通常是一个坏主意的方法将成为该情况的适当解决方案。如果您认为这是您的情况的最佳解决方案,请继续。 :) 在某些情况下,即使是单例和全局变量以及嵌套 if 也是一个好主意...
    • 这就是为什么你总是在你的 switch 语句中定义一个默认情况。
    • 您的方法(限制 UI 中可用的内容)与 Enum.IsDefined 具有相同的缺点:如果 UI 代码与枚举不同步,那么您可以获得超出范围的值 (即未在枚举中声明)。尽管仍然需要处理这些值(例如在switch 语句中使用default 案例),但在将值存储到对象的字段中之前使用Enum.IsDefined 似乎是一种好习惯:它允许及早发现错误,而不是让它四处飘荡,直到不再清楚虚假值的来源。
    【解决方案6】:

    此答案是对 deegee 的回答的回应,该回答提出了 System.Enum 的性能问题,因此不应将其视为我首选的通用答案,更多地解决严格性能场景中的枚举验证。

    如果您遇到关键任务性能问题,其中缓慢但功能性代码在紧密循环中运行,那么我个人会考虑尽可能将代码移出循环,而不是通过减少功能来解决。将代码限制为仅支持连续枚举可能是寻找错误的噩梦,例如,如果将来有人决定弃用某些枚举值。简单地说,您可以在开始时调用 Enum.GetValues 一次,以避免触发所有反射等数千次。这应该可以立即提高性能。如果你需要更高的性能并且你知道你的很多枚举是连续的(但你仍然想支持'gappy'枚举),你可以更进一步,做一些类似的事情:

    public abstract class EnumValidator<TEnum> where TEnum : struct, IConvertible
    {
        protected static bool IsContiguous
        {
            get
            {
                int[] enumVals = Enum.GetValues(typeof(TEnum)).Cast<int>().ToArray();
    
                int lowest = enumVals.OrderBy(i => i).First();
                int highest = enumVals.OrderByDescending(i => i).First();
    
                return !Enumerable.Range(lowest, highest).Except(enumVals).Any();
            }
        }
    
        public static EnumValidator<TEnum> Create()
        {
            if (!typeof(TEnum).IsEnum)
            {
                throw new ArgumentException("Please use an enum!");
            }
    
            return IsContiguous ? (EnumValidator<TEnum>)new ContiguousEnumValidator<TEnum>() : new JumbledEnumValidator<TEnum>();
        }
    
        public abstract bool IsValid(int value);
    }
    
    public class JumbledEnumValidator<TEnum> : EnumValidator<TEnum> where TEnum : struct, IConvertible
    {
        private readonly int[] _values;
    
        public JumbledEnumValidator()
        {
            _values = Enum.GetValues(typeof (TEnum)).Cast<int>().ToArray();
        }
    
        public override bool IsValid(int value)
        {
            return _values.Contains(value);
        }
    }
    
    public class ContiguousEnumValidator<TEnum> : EnumValidator<TEnum> where TEnum : struct, IConvertible
    {
        private readonly int _highest;
        private readonly int _lowest;
    
        public ContiguousEnumValidator()
        {
            List<int> enumVals = Enum.GetValues(typeof (TEnum)).Cast<int>().ToList();
    
            _lowest = enumVals.OrderBy(i => i).First();
            _highest = enumVals.OrderByDescending(i => i).First();
        }
    
        public override bool IsValid(int value)
        {
            return value >= _lowest && value <= _highest;
        }
    }
    

    你的循环变成这样的地方:

    //Pre import-loop
    EnumValidator< MyEnum > enumValidator = EnumValidator< MyEnum >.Create();
    while(import)   //Tight RT loop.
    {
        bool isValid = enumValidator.IsValid(theValue);
    }
    

    我确信 EnumValidator 类可以更高效地编写(这只是一个快速演示),但坦率地说,谁在乎导入循环之外发生了什么?唯一需要超快的位是在循环内。这就是采用抽象类路线的原因,以避免循环中不必要的 if-enumContiguous-then-else (工厂 Create 基本上是预先完成的)。 您会注意到有点虚伪,为简洁起见,此代码将功能限制为 int-enums。我应该使用 IConvertible 而不是直接使用 int 但这个答案已经够罗嗦了!

    【讨论】:

    • 对于较大的枚举,您可以使用字典在 O(1) 时间内查找枚举值。
    【解决方案7】:

    这就是我根据网上多个帖子的做法。这样做的原因是为了确保标有Flags 属性的枚举也可以成功验证。

    public static TEnum ParseEnum<TEnum>(string valueString, string parameterName = null)
    {
        var parsed = (TEnum)Enum.Parse(typeof(TEnum), valueString, true);
        decimal d;
        if (!decimal.TryParse(parsed.ToString(), out d))
        {
            return parsed;
        }
    
        if (!string.IsNullOrEmpty(parameterName))
        {
            throw new ArgumentException(string.Format("Bad parameter value. Name: {0}, value: {1}", parameterName, valueString), parameterName);
        }
        else
        {
            throw new ArgumentException("Bad value. Value: " + valueString);
        }
    }
    

    【讨论】:

      【解决方案8】:

      根据 Timo 的回答,我精心设计了以下扩展方法(C# 6 语法)以提供快速、通用的解决方案。

      这避免了Enum.IsDefined 的性能问题,而且语法更简洁。

      public static class EnumHelpers
      {
          /// <summary>
          /// Returns whether the given enum value is a defined value for its type.
          /// </summary>
          public static bool IsDefined<T>(this T enumValue)
              where T : Enum
              => EnumValueCache<T>.DefinedValues.Contains(enumValue);
      
          /// <summary>
          /// Caches the defined values for each enum type for which this class is accessed.
          /// </summary>
          private static class EnumValueCache<T>
              where T : Enum
          {
              public static readonly HashSet<T> DefinedValues = new HashSet<T>((T[])Enum.GetValues(typeof(T)));
          }
      }
      

      用法:

      if (!myEnumValue.IsDefined())
         // ...
      

      【讨论】:

        【解决方案9】:

        您可以为您的项目使用 FluentValidation。这是“枚举验证”的一个简单示例

        让我们使用 FluentValidation 创建一个 EnumValidator 类;

        public class EnumValidator<TEnum> : AbstractValidator<TEnum> where TEnum : struct, IConvertible, IComparable, IFormattable
        {
            public EnumValidator(string message)
            {
                RuleFor(a => a).Must(a => typeof(TEnum).IsEnum).IsInEnum().WithMessage(message);
            }
        
        }
        

        现在我们创建了 enumvalidator 类;让我们创建一个类来调用 enumvalidor 类;

         public class Customer 
        {
          public string Name { get; set; }
          public Address address{ get; set; }
          public AddressType type {get; set;}
        }
        public class Address 
        {
          public string Line1 { get; set; }
          public string Line2 { get; set; }
          public string Town { get; set; }
          public string County { get; set; }
          public string Postcode { get; set; }
        

        }

        public enum AddressType
        {
           HOME,
           WORK
        }
        

        是时候为客户类中的地址类型调用我们的枚举验证器了。

        public class CustomerValidator : AbstractValidator<Customer>
        {
            public CustomerValidator()
           {
             RuleFor(x => x.type).SetValidator(new EnumValidator<AddressType>("errormessage");
          }
        }
        

        【讨论】:

          【解决方案10】:

          我发现这个link 回答得很好。它使用:

          (ENUMTYPE)Enum.ToObject(typeof(ENUMTYPE), INT)
          

          【讨论】:

          • 如果传入一个非枚举整数,这不会抛出异常吗?
          • @Richard - 是的,我写这篇文章已经 3 年了
          • 抛出异常也会造成性能损失。当有更好的解决方案时,你永远不应该抛出异常。
          【解决方案11】:

          要验证一个值是否是枚举中的有效值,只需要调用静态方法Enum.IsDefined即可。

          int value = 99;//Your int value
          if (Enum.IsDefined(typeof(your_enum_type), value))
          {
             //Todo when value is valid
          }else{
             //Todo when value is not valid
          }
          

          【讨论】:

          • 投反对票不是因为错了,而是因为晚了 8 年才写了一个答案,但没有提供任何新信息,而这些新信息不在已被接受的答案中。
          猜你喜欢
          • 2017-12-14
          • 1970-01-01
          • 2019-06-05
          • 2021-05-16
          • 1970-01-01
          • 2012-07-01
          • 1970-01-01
          • 2019-12-29
          • 1970-01-01
          相关资源
          最近更新 更多