Enum.HasFlag,为什么没有Enum.SetFlag?

48

我必须为我声明的每个标志类型构建一个扩展方法,就像这样:

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

    return flags;
}
为什么没有像Enum.HasFlag这样的Enum.SetFlag
此外,为什么这种方法不总是起作用?
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 吗?


16
@CodyGray,"|="运算符更简短,但假设熟悉二进制标志的实现,而SetFlag更直观。 - Michael Freidgeim
4
我喜欢你的问题,认为它非常合理。我希望有一天微软能够编写一个标准的 SetFlag 函数。谢谢你的提问。 - Eric Ouellet
9
@CodyGray 如果你的输入是一个布尔值和一个标志,难道不是只需要一行而不是四行代码吗?这足以激发写扩展方法的动力了吧?如果我需要四行代码才能将数字加1,我肯定会写一个AddOneToInteger方法。 - Juan
11个回答

59
为什么没有像Enum.HasFlag一样的Enum.SetFlag?
Enum.HasFlag作为一个按位操作需要更复杂的逻辑和重复两次相同的标志。
 myFlagsVariable=    ((myFlagsVariable & MyFlagsEnum.MyFlag) ==MyFlagsEnum.MyFlag );

所以微软决定实现它。

在C#中,SetFlag和ClearFlag很简洁。

    flags |= flag;// SetFlag

    flags &= ~flag; // ClearFlag 

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

应该创建SetFlag和ClearFlag扩展 - 是的,以便它们能够在智能提示中显示。

但是应该由开发人员使用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  ")

谢谢你的建议,但是使用扩展方法真的会影响性能吗?即使你每秒设置了成千上万个标志,这么简单的操作在编译时不会被内联吗? - g t
1
@gt:可能性能影响不是很重要,但用包括Enum.Parse在内的10行代码替换最简单的二进制操作看起来不正确。而且你不知道它将在哪个循环中使用。 - Michael Freidgeim
啊,我以为只是简单的二进制操作,但看了其他答案后我明白你的意思了。很遗憾C#枚举处理像这样棘手,看看Jon Skeet的UnconstrainedMelody有类似的解决方法还是挺有趣的。 - g t
4
请注意,“binary operations”一词您所指的实际上是“位运算”。二元运算是另一个概念,它只是表示有两个操作数(+ 也是一个二元运算符)。还有一元运算符(例如 ++)和三元运算符(到目前为止只有一个,即 ?:,如 a? b: c)。&| 位运算符都是二元运算符(需要两个操作数),而 ~ 是一元运算符(它只是反转其操作数的位)。 - MarioDS
1
我还缺少SetFlag,在这里解释并实现了它:https://gaevoy.com/2023/11/14/learn-enum-flags-csharp.html - undefined

14

我做了一些对我来说很有效且非常简单的事情...

这些事情涉及到IT技术。
    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);

11
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中你要检查null?据我所知,枚举类型是不可为空的。 - Michael Freidgeim
@MichaelFreidgeim,实际上枚举类型是不可为空的,但是Enum是一个装箱的引用类型表示。由于参数是引用类型,因此它可以为null。(当您使用ValueTypeObject基类来表示值类型时,也可能遇到此问题)。 - smartcaveman
这很好,但是有错误。在第一个方法IncludeAll中,累加器“result”没有累加,因为对“Include”的调用没有被设置回“result”变量中。我将编辑代码来解决这个问题。 - argyle

3
& 运算符在 a & bb & 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 & 0 == 0

3

现在是2021年,C#语言有许多好的特性,这意味着应该有更加优雅的方法来完成这个任务。让我们来讨论之前回答中的观点...

观点1: 关闭一个标志位的效率很低,因为它需要两个操作,并且调用另一个方法会增加额外的开销。

这个观点应该是错误的。 如果你添加了AggressiveInlining编译器标志,编译器应该会将按位运算提升为直接的内联操作。如果你正在编写关键代码,你可能需要进行基准测试以确认,因为结果甚至可以在不同的编译器版本之间有所不同。但是重点是,你应该能够调用一个便利的方法而不需要支付方法查找的成本。

观点2: 这种做法非常冗长,因为你必须设置标志并分配返回值。

这个观点也应该是错误的。C#提供了“ref”关键字,允许你通过引用直接操作值类型参数(在这种情况下是枚举)。结合AggressiveInlining,编译器应该足够聪明,完全删除ref指针,生成的IL代码应该与直接内联两个按位操作相同。

注意: 当然,这只是理论。也许其他人可以在评论中来检查所提出的代码的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);

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

3

这里有另一种快速的方法来为任何枚举类型设置标志:

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的较重答案使用动态转换处理未知的枚举类型。 - Etherman

2
回答你的问题的一部分:Get函数根据二进制逻辑正常工作-它检查任何匹配项。如果您想匹配整个标志集,请考虑使用以下方法:
return ((flags & flag) != flag);

关于“为什么没有SetFlag”...可能是因为它并不是真正需要的。标志是整数。已经有一种处理这些标志的约定,并且同样适用于标志。如果您不想使用|&编写它 - 那就是自定义静态插件所用的方式 - 您可以像您自己演示的那样使用自己的函数 :)


5
你可以说 HasFlag 也是同样的情况,但它确实存在。 - bevacqua
@Nico 确实,但是看一下msdn上的评论:“效率警告 使用此方法的用户应该意识到当前的实现非常慢,大约比手动内联代码慢1000倍,因此不建议在性能关键的代码中使用。”为什么存在这种不一致性可能是一个问题,需要向微软的开发人员提问,而不是向Stack Overflow提问 :( - viraptor
圣诞节的巧克力,链接呢?太糟糕了! - bevacqua
@Nico http://msdn.microsoft.com/en-us/library/system.enum.hasflag.aspx#2 - 我认为&只涉及二进制操作。HasFlag涉及将值复制到堆栈、进入、二进制操作、复制结果、返回、从堆栈分配等步骤。(该链接是社区评论,所以YMMV等等)。 - viraptor
@Bolt:Enum.HasFlag速度较慢的原因是它使用反射进行一些额外的类型检查。问题不在于它是一个单独的方法调用;这显然远远不足以解释你看到的开销。由于我仍然针对.NET 3.5(没有方便的HasFlag方法),我编写了自己的代码,不使用反射(至少在发布版本中不使用),并且我看到与上面内联编写的代码性能相当。(我浪费了比Donald Knuth批准的基准测试更多的时间。) - Cody Gray
显示剩余3条评论

2
Enums在很久以前就被C语言搞砸了。在C#语言中具有一定的类型安全性对设计者来说非常重要,当底层类型可以是从byte到long的任何类型时,就没有Enum.SetFlags的余地了。这是另一个由C引起的问题。
正确的处理方式是显式地内联编写此类代码,而不尝试将其塞入扩展方法中。您不想在C#语言中编写C宏。

为什么枚举可以是从byte到long的任何类型,这是C语言引起的问题吗?这似乎是任何语言都具有的便利功能。 - Cody Gray
我不理解你的回答。Set flag应该接受一个或多个标志和一个布尔值,其中true表示设置了标志,false表示未设置。这与C语言或底层类型有什么关系??? - Eric Ouellet
类型安全的本质是你永远不会向变量中写入错误数量的字节。.NET枚举可以是1、2、4或8个字节。因此,编写扩展方法变得困难,它必须为任何枚举类型写入正确数量的字节。它只能通过使用反射来实现这一点,反射是唯一可以确定枚举类型使用了多少字节的方法。这使得编写枚举变得容易增加两个数量级的开销。 - Hans Passant
非常感谢。我现在正在尝试编写SetFlag并查看你所说的内容! - Eric Ouellet
@Hans,感谢你的想法。我已经发布了一种方法来实现它。不知道你能否告诉我你对此的看法?它并不完美,但在我的情况下可以工作(并支持long、int等)。你认为有什么缺陷? - Eric Ouellet
反射不是确定类型大小的唯一方法,至少现在不是了。您可以使用sizeof(Type)并在不安全代码中解决类型安全值更改问题,请参见此解决方案:https://dev59.com/fG025IYBdhLWcg3wsIMU#68159618 - Martin Tilo Schmitz

1

目前为止,回答都很好,但如果您正在寻找一种更高效的速记方式,而不需要分配托管内存,您可以使用以下方法:

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;

0
这是一个改进的通用方法,不需要您在编译器设置中设置/unsafe,而是利用了Martin Tilo Schmitz的函数的方式。同时使用了System.Runtime.CompilerServices.Unsafe。
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SetFlag<T>(this ref T @enum, T flag) where T : unmanaged, Enum
{
    if (Unsafe.SizeOf<T>() == 4) // match default enum size first
        Unsafe.As<T, uint>(ref @enum) |= Unsafe.As<T, uint>(ref flag);
    else if (Unsafe.SizeOf<T>() == 8) // enum * : long
        Unsafe.As<T, ulong>(ref @enum) |= Unsafe.As<T, ulong>(ref flag);
    else if (Unsafe.SizeOf<T>() == 1)
        Unsafe.As<T, byte>(ref @enum) |= Unsafe.As<T, byte>(ref flag);
    else if (Unsafe.SizeOf<T>() == 2)
        Unsafe.As<T, short>(ref @enum) |= Unsafe.As<T, short>(ref flag);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnsetFlag<T>(this ref T @enum, T flag) where T : unmanaged, Enum
{
    if (Unsafe.SizeOf<T>() == 4) // match default enum size first
        Unsafe.As<T, uint>(ref @enum) &= ~Unsafe.As<T, uint>(ref flag);
    else if (Unsafe.SizeOf<T>() == 8) // enum * : long
        Unsafe.As<T, ulong>(ref @enum) &= ~Unsafe.As<T, ulong>(ref flag);
    else if (Unsafe.SizeOf<T>() == 1)
        Unsafe.As<T, byte>(ref @enum) &= (byte)~Unsafe.As<T, byte>(ref flag);
    else if (Unsafe.SizeOf<T>() == 2)
        Unsafe.As<T, short>(ref @enum) &= (short)~Unsafe.As<T, short>(ref flag);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接