![](/img/trans.png)
[英]Why does HasFlag extension method on Enum trump 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
),在試圖解釋它時會混淆這種情況。 人們似乎認為,因為Enum
和ValueType
在值類型的繼承鏈中,裝箱突然不適用了。 如果這是真的,那么可以說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()
需要類型信息,而不僅僅是值,對於this
和flag
。
大多數情況下, 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
。 我想參考lhs
, rhs
會阻止優化 function 變量的存儲。 枚舉大小的運行時分派增加了另一個開銷。HasFlag
快一個數量級。HasFlag
,性能優勢幾乎為零。unsafe { }
和使用class Unsafe
之間沒有顯着差異。 只有沒有優化class Unsafe
幾乎和HasFlag
一樣慢。MethodImplOptions.AggressiveInlining
沒有增加任何價值。仍然沒有真正快速且可讀的實現來測試枚舉中的標志。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.