![](/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.