为啥 Enum 的 HasFlag 方法需要装箱?

Posted

技术标签:

【中文标题】为啥 Enum 的 HasFlag 方法需要装箱?【英文标题】:Why Enum's HasFlag method need boxing?为什么 Enum 的 HasFlag 方法需要装箱? 【发布时间】:2012-07-26 08:27:30 【问题描述】:

我正在阅读“C# via CLR”,在第 380 页,有一条说明如下:

注意Enum类定义了一个HasFlag方法,定义如下

public Boolean HasFlag(Enum flag);

使用此方法,您可以像这样重写对 Console.WriteLine 的调用:

Console.WriteLine("Is 0 hidden? 1", file, attributes.HasFlag(FileAttributes.Hidden));

但是,出于这个原因,我建议您避免使用 HasFlag 方法:

因为它需要一个 Enum 类型的参数,您传递给它的任何值都必须装箱,需要分配内存。"

我无法理解这个加粗的陈述——为什么是“

您传递给它的任何值都必须装箱

flag参数类型是Enum,是值类型,为什么会有装箱? “您传递给它的任何值都必须装箱”应该意味着当您将值类型传递给参数Enum flag时会发生装箱,对吧?

【问题讨论】:

这一切都归结为一个单一但令人困惑的声明:Enum is not an enum... @MarcGravell 事实上,我已经花费了一长串的 cmets 试图为我的答案辩护,因为人们拒绝相信这一说法。迷惑者:ValueType 不是值类型哈哈... 请注意,从 .NET Core 2.1 开始,Enum.HasFlag 不会装箱,我相信:blogs.msdn.microsoft.com/dotnet/2018/04/18/…。虽然我可以在 2.1 应用程序中看到 IL 中的 box 指令,但它没有分配,因此我没有看到性能损失。 【参考方案1】:

值得注意的是,一个比Enum.HasFlag 扩展方法快大约30 倍的通用HasFlag<T>(T thing, T flags) 可以用大约30 行代码编写。甚至可以做成扩展方法。不幸的是,在 C# 中不可能将这种方法限制为只采用枚举类型的东西。因此,即使对于不适用的类型,Intellisense 也会弹出该方法。我认为如果有人使用 C# 或 vb.net 以外的其他语言来编写扩展方法,则可能仅在应该弹出时才弹出它,但我对其他语言还不够熟悉,无法尝试这样的事情。

internal static class EnumHelper<T1>

    public static Func<T1, T1, bool> TestOverlapProc = initProc;
    public static bool Overlaps(SByte p1, SByte p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(Byte p1, Byte p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(Int16 p1, Int16 p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(UInt16 p1, UInt16 p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(Int32 p1, Int32 p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(UInt32 p1, UInt32 p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(Int64 p1, Int64 p2)  return (p1 & p2) != 0; 
    public static bool Overlaps(UInt64 p1, UInt64 p2)  return (p1 & p2) != 0; 
    public static bool initProc(T1 p1, T1 p2)
    
        Type typ1 = typeof(T1);
        if (typ1.IsEnum) typ1 = Enum.GetUnderlyingType(typ1);
        Type[] types =  typ1, typ1 ;
        var method = typeof(EnumHelper<T1>).GetMethod("Overlaps", types);
        if (method == null) method = typeof(T1).GetMethod("Overlaps", types);
        if (method == null) throw new MissingMethodException("Unknown type of enum");
        TestOverlapProc = (Func<T1, T1, bool>)Delegate.CreateDelegate(typeof(Func<T1, T1, bool>), method);
        return TestOverlapProc(p1, p2);
    

static class EnumHelper

    public static bool Overlaps<T>(this T p1, T p2) where T : struct
    
        return EnumHelper<T>.TestOverlapProc(p1, p2);
    

编辑:以前的版本已损坏,因为它使用(或至少尝试使用)EnumHelper&lt;T1, T1&gt;

【讨论】:

这太神奇了!要去偷它:) 我认为你绝对应该在这里回答:***.com/questions/9519596/hasflag-with-a-generic-enum 缓存 method 会使其更快。 您可以在C# 7.3中应用 System.Enum 约束! @IvanGarcíaTopete 如果您像我一样坚持使用旧版本,您可以使用 struct 作为约束,并且至少可以防止 Intellisense 为基本上 任何东西提供扩展方法. @supercat:尽管是免费分配的,但在我的测试中,您的解决方案比原生 HasFlag 慢两倍(在单元测试中循环调用每个 10,000,000 次)。难道是打电话给代表超过了拳击的成本吗?还是我的单元测试具有误导性?【参考方案2】:

在这种情况下,在您进入HasFlags 方法之前,需要两次装箱调用。一种是将值类型的方法调用解析为基类型方法,另一种是将值类型作为引用类型参数传递。如果您执行var type = 1.GetType();,您可以在IL 中看到相同的结果,文字int 1 在GetType() 调用之前被装箱。方法调用的装箱似乎只有在值类型定义本身中没有覆盖方法时,可以在此处阅读更多内容:Does calling a method on a value type result in boxing in .NET?

HasFlags 接受Enum class 参数,因此这里会发生装箱。您正在尝试将 value type 传递给期望引用类型的内容。为了将值表示为引用,需要进行装箱。

对于值类型及其继承(Enum/ValueType)有很多编译器支持,在试图解释它时会混淆情况。人们似乎认为,因为EnumValueType 在值类型的继承链中,装箱突然不适用了。如果这是真的,那么object 也可以这样说,因为一切都继承了它——但我们知道这是错误的。

这并不能阻止将值类型表示为引用类型会导致装箱。

我们可以在 IL 中证明这一点(查找 box 代码):

class Program

    static void Main(string[] args)
    
        var f = Fruit.Apple;
        var result = f.HasFlag(Fruit.Apple);

        Console.ReadLine();
    


[Flags]
enum Fruit

    Apple




.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 

    // Method begins at RVA 0x2050
    // Code size 28 (0x1c)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] valuetype ConsoleApplication1.Fruit f,
        [1] bool result
    )

    IL_0000: nop
    IL_0001: ldc.i4.0
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: box ConsoleApplication1.Fruit
    IL_0009: ldc.i4.0
    IL_000a: box ConsoleApplication1.Fruit
    IL_000f: call instance bool [mscorlib]System.Enum::HasFlag(class [mscorlib]System.Enum)
    IL_0014: stloc.1
    IL_0015: call string [mscorlib]System.Console::ReadLine()
    IL_001a: pop
    IL_001b: ret
 // end of method Program::Main

表示一个值类型为ValueType时也可以看出,同样会导致装箱:

class Program

    static void Main(string[] args)
    
        int i = 1;
        ValueType v = i;

        Console.ReadLine();
    



.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 

    // Method begins at RVA 0x2050
    // Code size 17 (0x11)
    .maxstack 1
    .entrypoint
    .locals init (
        [0] int32 i,
        [1] class [mscorlib]System.ValueType v
    )

    IL_0000: nop
    IL_0001: ldc.i4.1
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: box [mscorlib]System.Int32
    IL_0009: stloc.1
    IL_000a: call string [mscorlib]System.Console::ReadLine()
    IL_000f: pop
    IL_0010: ret
 // end of method Program::Main

【讨论】:

是的,你是对的,但在这种情况下,由于调用 Console.WriteLine 而发生装箱。 Jeff Richter 在书中有很大一部分是关于避免拳击的,我相信这就是它的由来。 @stevethethread 那么为什么我的示例中有拳击 IL 命令? 这不是答案,这只是重复导致这个问题的观察结果。 @hvd 是的,Enum、enum 和 ValueType 的文档说明了继承链,但没有提供编译器如何实现这一点。 @sblom 错误信息在人们暗示从EnumValueType 继承意味着应该在EnumValueType 中表示这些值类型不会招致拳击......简单地说,它确实如此。可以为object 提出相同的论点。答案真的很简单:它装箱是因为您将值类型表示为引用类型。【参考方案3】:

Enum 继承自 ValueType,这是...一个类!因此,拳击。

请注意,Enum 类可以将任何枚举表示为装箱值,无论其基础类型是什么。而FileAttributes.Hidden 等值将表示为实值类型 int。

编辑:让我们在这里区分类型和表示。 int 在内存中表示为 32 位。它的类型派生自ValueType。一旦您将int 分配给object 或派生类(ValueType 类、Enum 类),您就将其装箱,有效地将其表示更改为现在包含该 32 位的类,以及额外的班级信息。

【讨论】:

我不明白你的第一句话。 void f(int i) void g() f(3); -- int 也继承自 ValueType,但那里没有装箱。如果将 int 更改为具体的枚举类型,则相同。 这不可能是全部。 System.Int32 也继承自 ValueType 是的,这里的方法需要一个int,而不是object。 @JulienLebosquain 是的,确实如此。所有结构都继承自ValueType @JulienLebosquain 请参阅 msdn 上的结构页面 msdn.microsoft.com/en-us/library/saxz13w4.aspx :: 结构不能从另一个结构或类继承,也不能是类的基础。所有结构都直接继承自 System.ValueType,后者继承自 System.Object。【参考方案4】:

自 C# 7.3 以来,引入了通用枚举约束,您可以编写一个不依赖反射的快速、非分配版本。它需要编译器标志 /unsafe 但由于 Enum 支持类型只能是固定数量的大小,因此应该非常安全:

using System;
using System.Runtime.CompilerServices;
public static class EnumFlagExtensions

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool HasFlagUnsafe<TEnum>(TEnum lhs, TEnum rhs) where TEnum : unmanaged, Enum
    
        unsafe
        
            switch (sizeof(TEnum))
            
                case 1:
                    return (*(byte*)(&lhs) & *(byte*)(&rhs)) > 0;
                case 2:
                    return (*(ushort*)(&lhs) & *(ushort*)(&rhs)) > 0;
                case 4:
                    return (*(uint*)(&lhs) & *(uint*)(&rhs)) > 0;
                case 8:
                    return (*(ulong*)(&lhs) & *(ulong*)(&rhs)) > 0;
                default:
                    throw new Exception("Size does not match a known Enum backing type.");
            
        
    

【讨论】:

Unsafe.As&lt;TFrom, TTo&gt;() 应该允许不需要 /unsafe 编译器标志的实现。例如。 Unsafe.As&lt;TEnum, byte&gt;(ref lhs)MemoryMarshal.Cast&lt;TFrom, TTo&gt;() 也是如此,尽管我们首先必须使用 MemoryMarshal.CreateSpan&lt;T&gt;() 将值转换为跨度。)【参考方案5】:

当你传递一个将对象作为参数的方法的值类型时,就像console.writeline的情况一样,会有一个固有的装箱操作。 Jeffery Richter 在您提到的同一本书中详细讨论了这一点。

在这种情况下,您使用的是 console.writeline 的 string.format 方法,它采用 object[] 的 params 数组。所以你的布尔值将被转换为对象,因此你得到一个装箱操作。您可以通过在 bool 上调用 .ToString() 来避免这种情况。

【讨论】:

Enum.HasFlag的参数类型不是object 这是对Enum.HasFlag()bool 结果的装箱,而不是HasFlag() 的参数... Enum.HasFlag() 返回 bool、值类型,以及装箱。 @stevethethread 这不是这个问题的主题,此外,返回值类型不需要装箱,除非函数被声明为返回引用类型 (object)。跨度> 但是这里“你传递给它的任何值都必须被装箱”应该意味着当你将值类型传递给参数“枚举标志”时会发生装箱,对吧?【参考方案6】:

此调用涉及两个装箱操作,而不仅仅是一个。两者都是必需的,原因很简单:Enum.HasFlag() 需要 类型信息,而不仅仅是 thisflag 的值。

大多数时候,enum 值实际上只是一组位,编译器从方法签名中表示的 enum 类型中获得所需的所有类型信息。

但是,对于Enum.HasFlags(),它首先要做的是调用this.GetType()flag.GetType(),并确保它们相同。如果你想要无类型版本,你会问if ((attribute &amp; flag) != 0),而不是调用Enum.HasFlags()

【讨论】:

一段时间过去了,但无论如何:这不是真的。如果你在某个方法中请求GetType(),那么在这个方法中就会发生装箱。您可以使用简单的值类型轻松地测试它,例如调用 GetType() 的方法。您会看到装箱将在您的方法内部发生,而不是在您从某些外部代码调用它时发生。 @iw.kuchin:System.Enum 类型是类类型,具有讽刺意味的是 System.ValueType。调用Enum.HasFlag(Enum) 需要将其参数转换为System.Enum,这意味着它将在HasFlag 方法有机会执行之前被装箱。 @supercat 是的,这是真正的原因,因为这里发生了拳击。不是因为 Enum.HasFlag() 中的某处需要类型信息。【参考方案7】:

而且,Enum.HasFlag 不止是单拳:

public bool HasFlag(Enum flag)

    if (!base.GetType().IsEquivalentTo(flag.GetType()))
    
        throw new ArgumentException(Environment.GetResourceString("Argument_EnumTypeDoesNotMatch", new object[]
        
            flag.GetType(),
            base.GetType()
        ));
    
    ulong num = Enum.ToUInt64(flag.GetValue());
    ulong num2 = Enum.ToUInt64(this.GetValue());
    return (num2 & num) == num;

查看GetValue 方法调用。

更新。 看起来MS在.NET 4.5中优化了这个方法(源代码已从referencesource下载):

    [System.Security.SecuritySafeCritical]
    public Boolean HasFlag(Enum flag)  
        if (flag == null)
            throw new ArgumentNullException("flag"); 
        Contract.EndContractBlock(); 

        if (!this.GetType().IsEquivalentTo(flag.GetType()))  
            throw new ArgumentException(Environment.GetResourceString("Argument_EnumTypeDoesNotMatch", flag.GetType(), this.GetType()));
        

        return InternalHasFlag(flag); 
    

    [System.Security.SecurityCritical]  // auto-generated 
    [ResourceExposure(ResourceScope.None)]
    [MethodImplAttribute(MethodImplOptions.InternalCall)] 
    private extern bool InternalHasFlag(Enum flags);

【讨论】:

这是实际的实现吗?似乎不必要的慢。编写一个静态方法 bool HasFlag&lt;T&gt;(T p1, T p2) 是可能的,而且不是太难,它的运行速度大约是 enum.HasFlag 的 10 倍以上。 @supercat:确实是个很好的问题。对于 .NET 4.0 是实际的,对于 .NET 4.5 是不同的。查看更新的答案。 我想知道这对性能有何影响?使用 .net 4.0,我的通用方法似乎比使用 &amp; 慢 6 倍,但比 Enum.HasFlag 快 30 倍。实际上,我认为测试标志值的枚举将是一个足够频繁的操作,值得语言支持,特别是因为一种语言可以分离出HasAnyHasAllHas 情况,限制后者对于两个恒定幂的操作数 [因为在其他情况下 SomeEnum.Has(3) 应该表示 (SomeEnum &amp; 3) != 0 还是 (SomeEnum &amp; 3)==3 是模棱两可的。] 如果您想将其与 .net 4.5 版本的 HasFlag 进行基准测试,请查看我的代码答案。 (顺便说一下,如图所示,我的版本适用于定义 Overlaps(T,T) 重载的任何结构类型 T;不确定这是否有用,但它不应该影响基准,因为每种类型只评估一次)。【参考方案8】:

正如 Timo 所建议的,Martin Tilo Schmitz 的解决方案可以实现无需/unsafe 开关

public static bool HasAnyFlag<E>(this E lhs, E rhs) where E : unmanaged, Enum

    switch (Unsafe.SizeOf<E>())
    
    case 1:
        return (Unsafe.As<E, byte>(ref lhs) & Unsafe.As<E, byte>(ref rhs)) != 0;
    case 2:
        return (Unsafe.As<E, ushort>(ref lhs) & Unsafe.As<E, ushort>(ref rhs)) != 0;
    case 4:
        return (Unsafe.As<E, uint>(ref lhs) & Unsafe.As<E, uint>(ref rhs)) != 0;
    case 8:
        return (Unsafe.As<E, ulong>(ref lhs) & Unsafe.As<E, ulong>(ref rhs)) != 0;
    default:
        throw new Exception("Size does not match a known Enum backing type.");
    

需要 NuGet System.Runtime.CompilerServices.Unsafe 才能使用 .NET Framework 进行编译。

性能

我不得不提到,任何通用实现仍然比本机检查慢一个数量级,即((int)lhs &amp; (int)rhs) != 0。 我想引用lhsrhs 会阻止优化函数变量的存储。 枚举大小的运行时调度增加了另一个开销。 但它仍然HasFlag快一个数量级。 好吧,如果在调试版本中关闭优化,性能优势几乎为零,与 HasFlag 相比。 在优化(发布)版本中使用unsafe 和使用class Unsafe 之间没有显着差异。只有没有优化class Unsafe 几乎和HasFlag 一样慢。 推荐使用委托作为 supercat 是不合理的选择,因为在大多数 CPU 架构上生成的函数指针调用速度很慢,甚至完全阻止了内联。 MethodImplOptions.AggressiveInlining 没有任何价值。

结论

仍然没有真正快速可读的实现来测试枚举中的标志。

【讨论】:

以上是关于为啥 Enum 的 HasFlag 方法需要装箱?的主要内容,如果未能解决你的问题,请参考以下文章

为啥有些语言需要装箱和拆箱?

为啥自动装箱会使 Java 中的某些调用模棱两可?

为啥我们在 Java 中使用自动装箱和拆箱?

Enum<E> 的“潜在堆污染通过 varargs 参数”...为啥?

c# 泛型为啥能解决装箱拆箱问题

C#中的装箱事件