我正在阅读“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中看到相同的内容,在int
调用之前,文字GetType()
1被装箱。方法调用上的装箱似乎只是在值类型定义本身没有覆盖方法时,更多内容可以在这里阅读:Does calling a method on a value type result in boxing in .NET?
HasFlags
采用Enum
类参数,所以拳击将在这里发生。你试图将value type传递给期望引用类型的东西。为了将值表示为引用,发生装箱。
有很多编译器支持值类型及其继承(使用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
值得注意的是,比HasFlag<T>(T thing, T flags)
扩展方法快30倍的通用Enum.HasFlag
可以用大约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, 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);
}
}
Enum
继承自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()
需要this
和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);