【问题标题】:Enum.HasFlag, why no Enum.SetFlag?Enum.HasFlag,为什么没有 Enum.SetFlag?
【发布时间】:2021-06-28 15:57:22
【问题描述】:

我必须为我声明的每个标志类型构建一个扩展方法,如下所示:

public static EventMessageScope SetFlag(this EventMessageScope flags, 
    EventMessageScope flag, bool value)
{
    if (value)
        flags |= flag;
    else
        flags &= ~flag;

    return flags;
}

为什么没有Enum.SetFlag 就像有Enum.HasFlag 一样?

另外,为什么这并不总是有效?

public static bool Get(this EventMessageScope flags, EventMessageScope flag)
{
    return ((flags & flag) != 0);
}

例如,如果我有:

var flag = EventMessageScope.Private;

然后检查它:

if(flag.Get(EventMessageScope.Public))

EventMessageScope.Public 真的是EventMessageScope.Private | EventMessageScope.PublicOnly,它返回 true。

如果不是,因为Private 不是公开的,它只是半公开的。

同样适用:

if(flag.Get(EventMessageScope.None))

哪个返回false,除了范围实际上是None (0x0),什么时候它应该总是返回true?

【问题讨论】:

  • @CodyGray, |= 运算符更短,但假定熟悉标志的二进制实现,SetFlag 更直观。
  • 我喜欢你的问题,觉得它完全合乎逻辑。我希望有一天微软会编写一个标准的 SetFlag。感谢询问
  • @CodyGray 不是 1 行而不是 4 行(当你有一个布尔值和一个标志作为输入时)每次你想设置一个标志就足够了编写扩展方法的动机?如果我需要 4 行来为一个数字加 1,我肯定会写一个 AddOneToInteger 方法。

标签: c# enums flags


【解决方案1】:

为什么没有 Enum.SetFlag 就像有 Enum.HasFlag 一样?

HasFlag 作为按位运算需要更复杂的逻辑并重复相同的标志两次

 myFlagsVariable=    ((myFlagsVariable & MyFlagsEnum.MyFlag) ==MyFlagsEnum.MyFlag );

所以 MS 决定实施它。

SetFlag 和 ClearFlag 在 C# 中是简洁的

    flags |= flag;// SetFlag

    flags &= ~flag; // ClearFlag 

但不幸的是不直观。每次我需要设置(或清除)一个标志时,我都会花几秒钟(或几分钟)思考:方法的名称是什么?为什么它没有在智能感知中显示?或者不,我必须使用按位运算。注意,有些开发者也会问:什么是位运算?

是否应该创建 SetFlag 和 ClearFlag 扩展 - YES 会出现在智能感知中。

开发人员是否应该使用 SetFlag 和 ClearFlag 扩展 - 不,因为它们效率不高。

我们在库的 EnumFlagsHelper 类中创建了扩展,例如 SomeEnumHelperMethodsThatMakeDoingWhatYouWantEasier,但将函数命名为 SetFlag 而不是 Include 和 ClearFlag 而不是 Remove。

在 SetFlag 方法的主体(以及摘要注释中)我决定添加

Debug.Assert( false, " do not use the extension due to performance reason, use bitwise operation with the explanatory comment instead \n 
flags |= flag;// SetFlag")

类似的消息应该添加到ClearFlag

Debug.Assert( false, " do not use the extension due to performance reason, use bitwise operation with the explanatory comment instead \n 
         flags &= ~flag; // ClearFlag  ")

【讨论】:

  • +1 表示建议,但使用扩展方法真的会对性能产生影响吗?也许如果你每秒设置数千个标志,即使那样,这样一个简单的操作在编译期间不会被内联吗?
  • @gt:可能性能影响不是必需的,但是用包括 Enum.Parse 在内的 10 行代码替换最简单的二进制操作看起来不正确。而且你不知道它会在哪个循环中使用。
  • 啊,我认为这只是简短的二进制操作,但看看其他答案,我可以明白你的意思。一个耻辱的 C# 枚举处理就像这样很棘手,看看 Jon Skeet 的 UnconstrainedMelody 很有趣,可以看到类似的解决方法。
  • 请注意,您所说的“二元运算”实际上是“位运算”。二元运算是另一个概念,它只是意味着有两个操作数(+ 也是二元运算符)。还有一元运算符(例如++)和三元运算符(目前只有一种,即?:,如a ? b : c)。 &| 位运算符都是二元运算符(需要两个操作数),而 ~ 是一元运算符(它只是将其操作数的位取反)。
【解决方案2】:

我做了一些对我有用的事情,而且非常简单......

    public static T SetFlag<T>(this Enum value, T flag, bool set)
    {
        Type underlyingType = Enum.GetUnderlyingType(value.GetType());

        // note: AsInt mean: math integer vs enum (not the c# int type)
        dynamic valueAsInt = Convert.ChangeType(value, underlyingType);
        dynamic flagAsInt = Convert.ChangeType(flag, underlyingType);
        if (set)
        {
            valueAsInt |= flagAsInt;
        }
        else
        {
            valueAsInt &= ~flagAsInt;
        }

        return (T)valueAsInt;
    }

用法:

    var fa = FileAttributes.Normal;
    fa = fa.SetFlag(FileAttributes.Hidden, true);

【讨论】:

    【解决方案3】:
    public static class SomeEnumHelperMethodsThatMakeDoingWhatYouWantEasier
    {
        public static T IncludeAll<T>(this Enum value)
        {
            Type type = value.GetType();
            object result = value;
            string[] names = Enum.GetNames(type);
            foreach (var name in names)
            {
                ((Enum) result).Include(Enum.Parse(type, name));
            }
    
            return (T) result;
            //Enum.Parse(type, result.ToString());
        }
    
        /// <summary>
        /// Includes an enumerated type and returns the new value
        /// </summary>
        public static T Include<T>(this Enum value, T append)
        {
            Type type = value.GetType();
    
            //determine the values
            object result = value;
            var parsed = new _Value(append, type);
            if (parsed.Signed is long)
            {
                result = Convert.ToInt64(value) | (long) parsed.Signed;
            }
            else if (parsed.Unsigned is ulong)
            {
                result = Convert.ToUInt64(value) | (ulong) parsed.Unsigned;
            }
    
            //return the final value
            return (T) Enum.Parse(type, result.ToString());
        }
    
        /// <summary>
        /// Check to see if a flags enumeration has a specific flag set.
        /// </summary>
        /// <param name="variable">Flags enumeration to check</param>
        /// <param name="value">Flag to check for</param>
        /// <returns></returns>
        public static bool HasFlag(this Enum variable, Enum value)
        {
            if (variable == null)
                return false;
    
            if (value == null)
                throw new ArgumentNullException("value");
    
            // Not as good as the .NET 4 version of this function, 
            // but should be good enough
            if (!Enum.IsDefined(variable.GetType(), value))
            {
                throw new ArgumentException(string.Format(
                    "Enumeration type mismatch.  The flag is of type '{0}', " +
                    "was expecting '{1}'.", value.GetType(), 
                    variable.GetType()));
            }
    
            ulong num = Convert.ToUInt64(value);
            return ((Convert.ToUInt64(variable) & num) == num);
        }
    
    
        /// <summary>
        /// Removes an enumerated type and returns the new value
        /// </summary>
        public static T Remove<T>(this Enum value, T remove)
        {
            Type type = value.GetType();
    
            //determine the values
            object result = value;
            var parsed = new _Value(remove, type);
            if (parsed.Signed is long)
            {
                result = Convert.ToInt64(value) & ~(long) parsed.Signed;
            }
            else if (parsed.Unsigned is ulong)
            {
                result = Convert.ToUInt64(value) & ~(ulong) parsed.Unsigned;
            }
    
            //return the final value
            return (T) Enum.Parse(type, result.ToString());
        }
    
        //class to simplfy narrowing values between
        //a ulong and long since either value should
        //cover any lesser value
        private class _Value
        {
            //cached comparisons for tye to use
            private static readonly Type _UInt32 = typeof (long);
            private static readonly Type _UInt64 = typeof (ulong);
    
            public readonly long? Signed;
            public readonly ulong? Unsigned;
    
            public _Value(object value, Type type)
            {
                //make sure it is even an enum to work with
                if (!type.IsEnum)
                {
                    throw new ArgumentException(
                        "Value provided is not an enumerated type!");
                }
    
                //then check for the enumerated value
                Type compare = Enum.GetUnderlyingType(type);
    
                //if this is an unsigned long then the only
                //value that can hold it would be a ulong
                if (compare.Equals(_UInt32) || compare.Equals(_UInt64))
                {
                    Unsigned = Convert.ToUInt64(value);
                }
                    //otherwise, a long should cover anything else
                else
                {
                    Signed = Convert.ToInt64(value);
                }
            }
        }
    }
    

    【讨论】:

    • @smartcaveman,为什么在 SetFlag 中检查是否为空? Afaik,枚举不可为空
    • @MichaelFreidgeim,一个实际的枚举类型不能为空,但是Enum 是一个装箱的引用类型表示。由于参数是引用类型,所以可以为空。 (当您使用 ValueTypeObject 基类来表示值类型时,您可能会遇到同样的问题。
    • 这真的很好,但是有错误。在第一个方法 IncludeAll 中,累加器“result”没有累加,因为对“Include”的调用没有被设置回“result”变量。我将编辑代码来解决这个问题。
    【解决方案4】:

    &amp; 运算符将给你与a &amp; b 相同的答案,因为它会给你b &amp; a,所以

    (EventMessaageScope.Private).Get(EventMessageScope.Private | EventMessageScope.PublicOnly)

    和写法一样

    (EventMessageScope.Private | EventMessageScope.PublicOnly).Get(EventMessaageScope.Private)

    如果您只想知道该值是否与 EventMessaageScope.Public 相同,则只需使用 equals

    EventMessageScope.Private == EventMessageScope.Public

    您的方法将始终为(EventMessageScope.None).Get(EventMessaageScope.None) 返回false,因为None == 0 并且仅当AND 操作的结果为零时才返回true。 0 &amp; 0 == 0.

    【讨论】:

      【解决方案5】:

      这是另一种为任何枚举设置标志的快速而肮脏的方法:

      public static T SetFlag<T>(this T flags, T flag, bool value) where T : struct, IComparable, IFormattable, IConvertible
          {
              int flagsInt = flags.ToInt32(NumberFormatInfo.CurrentInfo);
              int flagInt = flag.ToInt32(NumberFormatInfo.CurrentInfo);
              if (value)
              {
                  flagsInt |= flagInt;
              }
              else
              {
                  flagsInt &= ~flagInt;
              }
              return (T)(Object)flagsInt;
          }
      

      【讨论】:

      • 这仅适用于默认基础类型 (int32) 的枚举。如果您声明例如枚举为 ulong 然后我们得到无效的转换错误。对于默认类型,这个答案更快,Eric Ouellet 的动态转换更重的答案可以处理未知的枚举类型。
      【解决方案6】:

      回答您的部分问题:Get 函数根据二进制逻辑正常工作 - 它检查任何匹配项。如果您想匹配整个标志集,请考虑改为:

      return ((flags & flag) != flag);
      

      关于“为什么不存在 SetFlag”...可能是因为它并不是真正需要的。标志是整数。已经有处理这些的约定,它也适用于标志。如果您不想使用 |&amp; 编写它 - 这就是自定义静态插件的用途 - 您可以使用自己的函数来演示自己:)

      【讨论】:

      • HasFlag 也可以这样说……但它确实存在
      • @Nico True - 但请查看 msdn 上的评论:“效率警告此方法的用户应该知道当前的实现速度非常慢,比手动内联代码慢大约 1000 倍方法被定义为扩展,因此不建议在性能关键代码中使用。”为什么不一致本身可能是 MS 家伙的问题,而不是 SO :(
      • 哇。进入方法,执行并返回要慢 1000 倍?这是一些观点。
      • 圣洁巧克力,有链接吗?太可怕了!
      • @Nico msdn.microsoft.com/en-us/library/system.enum.hasflag.aspx#2 - 我想&amp; 只涉及二进制操作。 HasFlag涉及将值复制到堆栈,输入,二进制操作,复制结果,返回,从堆栈赋值。 (链接是社区评论,所以 YMMV 等等)
      【解决方案7】:

      枚举在很久以前就被 C 语言所困扰。在 C# 语言中具有一点类型安全性对设计人员来说很重要,当基础类型可以是字节和长整数之间的任何内容时,没有为 Enum.SetFlags 留下空间。顺便说一句,另一个 C 引起的问题。

      处理它的正确方法是显式地内联编写这种代码,并且尝试将其塞入扩展方法中。您不想用 C# 语言编写 C 宏。

      【讨论】:

      • 为什么枚举可以是从字节到长的任何类型是 C 引起的问题?这似乎是任何语言的便利功能。
      • 我不明白你的回答。设置标志应该采用一个或多个标志和一个布尔值,其中 true 是标志设置和 false 未设置。与 C 或底层类型的连接在哪里???
      • 类型安全的本质是您永远不会将错误数量的字节写入变量。 .NET 枚举可以是 1、2、4 或 8 个字节。因此,编写扩展方法变得困难,它必须为 any 枚举类型写入正确的字节数。它只能通过使用反射来做到这一点,这是确定枚举类型使用了多少字节的唯一方法。这使得编写一个枚举简单两个数量级更昂贵。
      • 非常感谢。我现在正在尝试写一个 SetFlag,看看你在说什么!
      • @Hans,感谢您的想法。我已经发布了一种方法来做到这一点。我想知道你能不能告诉我你对此的看法?它并不完美,但它适用于我的情况(并支持 long、int 等)。你看到了什么缺陷?
      【解决方案8】:

      现在是 2021 年,C# 有很多不错的特性,这意味着应该有一种更优雅的方式来做到这一点。让我们讨论一下先前答案的主张...

      索赔 1: 关闭标志是低效的,因为它使用两个操作并调用另一个方法只会增加更多开销。

      这应该是错误的。 如果添加 AggressiveInlining 编译器标志,编译器应该将按位操作提升为直接内联操作。如果您正在编写关键代码,您可能需要对其进行基准测试以确认,因为即使在次要编译器版本之间结果也会有所不同。但关键是,您应该能够调用便捷方法而无需支付方法查找成本。

      权利要求 2: 它过于冗长,因为您必须设置标志然后分配返回值。

      这也应该是错误的。 C# 提供“ref”,它允许您通过引用(在本例中为枚举)直接操作值类型参数。结合 AggressiveInlining,编译器应该足够聪明,可以完全删除 ref 指针,并且生成的 IL 看起来应该与直接内联两个按位操作一样。

      注意事项: 当然,这都是理论。也许其他人可以在这里的 cmets 中出现,并从下面建议的代码中检查 IL。我自己没有足够的经验(也没有现在的时间)来查看假设的说法是否正确。但我认为这个答案仍然值得发布,因为事实是 C# 应该能够做我正在解释的事情。

      如果其他人可以确认这一点,我可以相应地更新答案。

      public enum MyCustomEnum : long
      {
          NO_FLAGS            = 0,
          SOME_FLAG           = 1,
          OTHER_FLAG          = 1 << 1,
          YET_ANOTHER_FLAG    = 1 << 2,
          ANOTHER STILL       = 1 << 3
      }
      
      public static class MyCustomEnumExt
      {
          [MethodImpl(MethodImplOptions.AggressiveInlining)]
          public static void TurnOFF(ref this MyCustomEnum status, MyCustomEnum flag)
              => status &= ~flag;
      
          [MethodImpl(MethodImplOptions.AggressiveInlining)]
          public static void TurnON(ref this MyCustomEnum status, MyCustomEnum flag)
              => status |= flag;
      }
      

      你应该可以像这样使用代码:

      //Notice you don't have to return a value from the extension methods to assign manually.
      MyCustomEnum mc = MyCustomEnum.SOME_FLAG;
      mc.TurnOFF(MyCustomEnum.SOME_FLAG);
      mc.TurnON(MyCustomEnum.OTHER_FLAG);
      

      即使编译器未能正确优化它,它仍然非常方便。至少您可以在非关键代码中使用它并期望具有出色的可读性。

      【讨论】:

        【解决方案9】:

        到目前为止的答案很好,但如果您正在寻找一种不分配托管内存的性能更高的速记,您可以使用这个:

        using System;
        using System.Runtime.CompilerServices;
        public static class EnumFlagExtensions
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public static TEnum AddFlag<TEnum>(this TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
            {
                unsafe
                {
                    switch (sizeof(TEnum))
                    {
                        case 1:
                            {
                                var r = *(byte*)(&lhs) | *(byte*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 2:
                            {
                                var r = *(ushort*)(&lhs) | *(ushort*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 4:
                            {
                                var r = *(uint*)(&lhs) | *(uint*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 8:
                            {
                                var r = *(ulong*)(&lhs) | *(ulong*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        default:
                            throw new Exception("Size does not match a known Enum backing type.");
                    }
                }
            }
         
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public static TEnum RemoveFlag<TEnum>(this TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
            {
                unsafe
                {
                    switch (sizeof(TEnum))
                    {
                        case 1:
                            {
                                var r = *(byte*)(&lhs) & ~*(byte*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 2:
                            {
                                var r = *(ushort*)(&lhs) & ~*(ushort*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 4:
                            {
                                var r = *(uint*)(&lhs) & ~*(uint*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        case 8:
                            {
                                var r = *(ulong*)(&lhs) & ~*(ulong*)(&rhs);
                                return *(TEnum*)&r;
                            }
                        default:
                            throw new Exception("Size does not match a known Enum backing type.");
                    }
                }
         
            }
         
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public static void SetFlag<TEnum>(ref this TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
            {
                unsafe
                {
                    fixed (TEnum* lhs1 = &lhs)
                    {
                        switch (sizeof(TEnum))
                        {
                            case 1:
                                {
                                    var r = *(byte*)(lhs1) | *(byte*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 2:
                                {
                                    var r = *(ushort*)(lhs1) | *(ushort*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 4:
                                {
                                    var r = *(uint*)(lhs1) | *(uint*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 8:
                                {
                                    var r = *(ulong*)(lhs1) | *(ulong*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            default:
                                throw new Exception("Size does not match a known Enum backing type.");
                        }
                    }
                }
            }
         
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public static void ClearFlag<TEnum>(this ref TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
            {
                unsafe
                {
                    fixed (TEnum* lhs1 = &lhs)
                    {
                        switch (sizeof(TEnum))
                        {
                            case 1:
                                {
                                    var r = *(byte*)(lhs1) & ~*(byte*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 2:
                                {
                                    var r = *(ushort*)(lhs1) & ~*(ushort*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 4:
                                {
                                    var r = *(uint*)(lhs1) & ~*(uint*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            case 8:
                                {
                                    var r = *(ulong*)(lhs1) & ~*(ulong*)(&rhs);
                                    *lhs1 = *(TEnum*)&r;
                                    return;
                                }
                            default:
                                throw new Exception("Size does not match a known Enum backing type.");
                        }
                    }
                }
            }
        }
        

        它只需要 C# 7.3 或更高版本,并指示编译器接受 /unsafe 代码。

        AddFlag 和 RemoveFlag 不会修改您调用它的枚举值,SetFlag 和 ClearFlag 会修改它。这可能是性能开销最低的通用解决方案,但它仍然不会像直接使用那样快

        flags |= flag;
        flags &= ~flag;
        

        【讨论】:

          【解决方案10】:

          我发现的原因是,由于 enum 是一种值类型,因此您无法将其传入并设置其类型。对于所有认为它愚蠢的人,我对你们说:并非所有开发人员都了解位标志以及如何打开或关闭它们(这很不直观)。

          不是一个愚蠢的想法,只是不可能。

          【讨论】:

          猜你喜欢
          • 2011-11-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-11-24
          • 2015-06-20
          • 2010-12-10
          相关资源
          最近更新 更多