簡體   English   中英

為什么 Enum 的 HasFlag 方法需要裝箱?

[英]Why Enum's HasFlag method need boxing?

我正在閱讀“C# via CLR”,在第 380 頁上,有一條說明如下:

注意 Enum class 定義了一個 HasFlag 方法,定義如下

public Boolean HasFlag(Enum flag);

使用此方法,您可以像這樣重寫對 Console.WriteLine 的調用:

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

但是,出於這個原因,我建議您避免使用 HasFlag 方法:

由於它采用 Enum 類型的參數,因此您傳遞給它的任何值都必須裝箱,需要 memory 分配。”

我無法理解這個加粗的聲明——為什么“

您傳遞給它的任何值都必須裝箱

flag參數類型是Enum ,是值類型,為什么會有裝箱? “您傳遞給它的任何值都必須裝箱”應該意味着裝箱發生在您將值類型傳遞給參數Enum flag時,對嗎?

值得注意的是,一個比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<T1 , T1 >

在這種情況下,在進入HasFlags方法之前,需要兩次裝箱調用。 一種是將值類型的方法調用解析為基類型方法,另一種是將值類型作為引用類型參數傳遞。 如果您執行var type = 1.GetType();您可以在 IL 中看到相同的內容var type = 1.GetType(); ,文字int 1 在GetType()調用之前被裝箱。 方法調用上的裝箱似乎只有在值類型定義本身中沒有覆蓋方法時,可以在此處閱讀更多內容: Does calls a method on a value type result in boxing in .NET?

HasFlags接受一個Enum參數,所以這里會發生裝箱。 您正在嘗試將值類型傳遞給需要引用類型的內容。 為了將值表示為引用,會發生裝箱。

有很多編譯器支持值類型及其繼承(使用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

Enum繼承自ValueType ,它是……一個類! 因此拳擊。

請注意, Enum類可以將任何枚舉表示為裝箱值,無論其底層類型是什么。 而諸如FileAttributes.Hidden的值將表示為實值類型 int。

編輯:讓我們在這里區分類型和表示。 int在內存中表示為 32 位。 它的類型源自ValueType 一旦將int分配給object或派生類( ValueType類、 Enum類),您就將其裝箱,有效地將其表示形式更改為現在包含該 32 位以及附加類信息的類。

當你傳遞一個將對象作為參數的方法的值類型時,比如在 console.writeline 的情況下,將會有一個固有的裝箱操作。 Jeffery Richter 在您提到的同一本書中詳細討論了這一點。

在這種情況下,您使用的是console.writeline 的string.format 方法,該方法采用object[] 的params 數組。 所以你的 bool 將被轉換為對象,因此你得到一個裝箱操作。 您可以通過在 bool 上調用 .ToString() 來避免這種情況。

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

這個調用涉及兩個裝箱操作,而不僅僅是一個。 兩者都是必需的,原因很簡單: Enum.HasFlag()需要類型信息,而不僅僅是值,對於thisflag

大多數情況下, enum值實際上只是一組位,編譯器從方法簽名中表示的enum類型中獲得了它需要的所有類型信息。

然而,在Enum.HasFlags()的情況下,它所做的第一件事就是調用this.GetType()flag.GetType()並確保它們是相同的。 如果你想要無類型版本,你會問if ((attribute & flag) != 0) ,而不是調用Enum.HasFlags()

從 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.");
            }
        }
    }
}

正如 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 & (int)rhs) != 0 我想參考lhsrhs會阻止優化 function 變量的存儲。 枚舉大小的運行時分派增加了另一個開銷。
  • 但它仍然HasFlag快一個數量級
  • 好吧,如果在調試構建中關閉優化,則與HasFlag ,性能優勢幾乎為零。
  • 在優化(發布)構建中使用unsafe { }和使用class Unsafe之間沒有顯着差異。 只有沒有優化class Unsafe幾乎和HasFlag一樣慢。
  • 使用委托作為 supercat 推薦不是合理的選擇,因為在大多數 CPU 架構上生成的 function 指針調用很慢,甚至更完全阻止內聯。
  • MethodImplOptions.AggressiveInlining沒有增加任何價值。

結論

仍然沒有真正快速可讀的實現來測試枚舉中的標志。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM