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

时间:2012-07-26 08:27:31

标签: c# .net enums boxing

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

7 个答案:

答案 0 :(得分:8)

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

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

答案 1 :(得分:7)

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

HasFlags需要Enum 参数,因此拳击将在此处进行。您正试图将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

答案 2 :(得分:3)

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

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

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

此外,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);

答案 5 :(得分:0)

此次调用涉及两个拳击操作,而不仅仅是一个。由于一个简单的原因,两者都是必需的:{strong> Enum.HasFlag()需要类型信息,而不仅仅是thisflag的值。

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

然而,在Enum.HasFlags()的情况下,首先要做的是致电this.GetType()flag.GetType(),并确保它们完全相同。如果您想要无类型版本,则需要if ((attribute & flag) != 0),而不是调用Enum.HasFlags()

答案 6 :(得分:0)

自从 C# 7.3 引入通用 Enum 约束以来,您可以编写一个不依赖反射的快速、非分配版本。它需要编译器标志 /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.");
            }
        }
    }
}