为什么Enum的HasFlag方法需要拳击?

我正在阅读“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时会发生装箱,对吧?

在这个例子中,在你进入HasFlags方法之前需要两次装箱调用。 一种是将值类型的方法调用解析为基类型方法,另一种是将值类型作为引用类型参数传递。 如果你做var type = 1.GetType();你可以在IL中看到相同的内容var type = 1.GetType(); ,在GetType()调用之前,将文字int 1装箱。 方法调用上的装箱似乎只是在值类型定义本身没有覆盖方法的时候,可以在这里阅读更多: 在值类型上调用方法会导致在.NET中装箱吗?

HasFlags采用Enum 参数,因此拳击将在此处进行。 您正在尝试将值类型传递给期望引用类型的内容。 为了将值表示为引用,发生装箱。

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

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

我们可以在IL中certificate这一点(寻找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 

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

 internal static class EnumHelper { public static Func 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).GetMethod("Overlaps", types); if (method == null) method = typeof(T1).GetMethod("Overlaps", types); if (method == null) throw new MissingMethodException("Unknown type of enum"); TestOverlapProc = (Func)Delegate.CreateDelegate(typeof(Func), method); return TestOverlapProc(p1, p2); } } static class EnumHelper { public static bool Overlaps(this T p1, T p2) where T : struct { return EnumHelper.TestOverlapProc(p1, p2); } } 

Enuminheritance自ValueType ,它是一个类! 因此拳击。

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

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

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

在这种情况下,您使用的是console.writt的string.format方法,它采用了对象[]的params数组。 所以你的bool,将被施放到对象,所以你得到一个拳击操作。 你可以通过在bool上调用.ToString()来避免这种情况。

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

大多数情况下, enum值实际上只是一组位,编译器具有方法签名中表示的enum类型所需的所有类型信息。

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

而且,在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);