繁体   English   中英

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

[英]Enum.HasFlag, why no Enum.SetFlag?

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

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 EventMessageScope.Private | EventMessageScope.PublicOnly ,它返回 true。

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

这同样适用于:

if(flag.Get(EventMessageScope.None))

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

为什么没有像 Enum.HasFlag 那样的 Enum.SetFlag?

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

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

所以MS决定实施它。

SetFlag 和 ClearFlag 在 C# 中简洁明了

    flags |= flag;// SetFlag

    flags &= ~flag; // ClearFlag 

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

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

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

我们在库的 class EnumFlagsHelper 中创建了扩展,就像在SomeEnumHelperMethodsThatMakeDoingWhatYouWantEasier中一样,但将 function 命名为 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  ")

我做了一些对我有用的事情,这很简单......

    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);
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);
            }
        }
    }
}

这是对任何 Enum 的 SetFlag 的另一种快速而肮脏的方法:

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;
    }

&运算符会给你与a & b相同的答案,因为它会给你b & 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

要回答您的部分问题:Get function 根据二进制逻辑正常工作 - 它会检查任何匹配项。 如果要匹配整组标志,请考虑以下情况:

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

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

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

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

现在是 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);

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

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

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;

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM