CIL - Boxing/Unboxing 与可空
CIL - Boxing/Unboxing vs Nullable
如果我理解 CLR 装箱和处理可空值的方式,如 Boxing / Unboxing Nullable Types - Why this implementation? 所述,我仍然有一些困惑。例如,下面的 C# 7 代码
void C<T>(object o) where T : struct {
if (o is T t)
Console.WriteLine($"Argument is {typeof(T)}: {t}");
}
编译成如下CIL
IL_0000: ldarg.0
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!!T>
IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<!!T>
IL_000b: stloc.1
IL_000c: ldloca.s 1
IL_000e: call instance !0 valuetype [mscorlib]System.Nullable`1<!!T>::GetValueOrDefault()
IL_0013: stloc.0
IL_0014: ldloca.s 1
IL_0016: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_001b: brfalse.s IL_003c
IL_001d: ldstr "Argument is {0}: {1}"
IL_0022: ldtoken !!T
IL_0027: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_002c: ldloc.0
IL_002d: box !!T
IL_0032: call string [mscorlib]System.String::Format(string, object, object)
IL_0037: call void [mscorlib]System.Console::WriteLine(string)
IL_003c: ret
还有下面的 C#
void D<T>(object o) where T : struct {
if (o is T)
Console.WriteLine($"Argument is {typeof(T)}: {(T) o}");
}
编译成如下CIL
IL_0000: ldarg.0
IL_0001: isinst !!T
IL_0006: brfalse.s IL_002c
IL_0008: ldstr "Argument is {0}: {1}"
IL_000d: ldtoken !!T
IL_0012: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0017: ldarg.0
IL_0018: unbox.any !!T
IL_001d: box !!T
IL_0022: call string [mscorlib]System.String::Format(string, object, object)
IL_0027: call void [mscorlib]System.Console::WriteLine(string)
IL_002c: ret
我认为正在发生的事情:查看第一种方法的CIL,似乎(1)检查参数是否是 [boxed?] Nullable<T>
,如果是,则将其压入堆栈,否则 null
,(2) 将其拆箱(如果是 null
怎么办?),(3) 尝试获取它的值,否则 default(T)
,(4) 然后检查它是否有值,如果没有则分支。第二种方法的 CIL 非常简单,它只是尝试对参数进行拆箱。
如果两种代码的语义相同,为什么前一种情况涉及拆箱到 Nullable<T>
而前一种情况 "just unboxes"?其次,在第一个 CIL 中,如果对象参数是一个盒装 int
,我目前认为这正是它在罐子上所说的(即盒装 int
而不是盒装 Nullable<int>
), isinst
指令不会总是失败吗? Nullable<T>
即使在 CIL 级别也能得到特殊待遇吗?
更新:手写了一些MSIL后,好像object
,如果确实是盒装的int
,可以拆箱成int
或 Nullable<int>
.
.method private static void Foo(object o) cil managed {
.maxstack 1
ldarg.0
isinst int32
brfalse.s L_00
ldarg.0
unbox.any int32
call void [mscorlib]System.Console::WriteLine(int32)
L_00:
ldarg.0
isinst valuetype [mscorlib]System.Nullable`1<int32>
brfalse.s L_01
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
call void [mscorlib]System.Console::WriteLine(int32)
L_01:
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
brtrue.s L_02
ldstr "No value!"
call void [mscorlib]System.Console::WriteLine(string)
L_02:
ret
}
C# 7 中的新语法同时进行类型检查和类型转换。在旧版本中,这通常以两种可能的方式完成。
if(o is T)
//use (T)o
T t = o as T;
if(t != null)
//use t
对于引用类型,第一个有多余的转换,因为 is
被编译为 isinst
和一个条件分支,从你使用的 CIL 指令中可以看出。第二个代码在 CIL 方面与第一个相同,减去额外的 (T)o
转换(编译为 castclass
)。
对于值类型,第二个选项只能用可空类型来完成,我也认为它实际上比第一个慢一些(必须创建一个结构)。
我已经将以下方法编译成CIL:
static void C<T>(object o) where T : struct
{
T? t = o as T?;
if(t != null)
Console.WriteLine("Argument is {0}: {1}", typeof(T), t);
}
生成此代码:
.method private hidebysig static void C<valuetype .ctor ([mscorlib]System.ValueType) T>(object o) cil managed
{
// Code size 48 (0x30)
.maxstack 3
.locals init (valuetype [mscorlib]System.Nullable`1<!!T> V_0)
IL_0000: ldarg.0
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!!T>
IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<!!T>
IL_000b: stloc.0
IL_000c: ldloca.s V_0
IL_000e: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_0013: brfalse.s IL_002f
IL_0015: ldstr "Argument is {0}: {1}"
IL_001a: ldtoken !!T
IL_001f: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0024: ldloc.0
IL_0025: box valuetype [mscorlib]System.Nullable`1<!!T>
IL_002a: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002f: ret
}
这正是问题中的代码,除了调用 GetValueOrDefault,因为我没有获得可空实例的实际值。
可空类型不能直接装箱或拆箱,只能通过其基础值或作为普通空值。第一个 isinst
确保其他类型不会产生异常(我想 isinst !!T
也可以使用),而只是空引用。然后 unbox.any
操作码根据引用形成一个可为空的实例,然后像往常一样使用它。该指令也可以写成 null 检查并自行形成可为 null 的实例,但这样更短。
C# 7 使用第二种方式 is T t
,因此如果 T
是值类型,它除了使用可空类型别无选择。为什么不选择前一个选项?我只能猜测它在语义或实现、变量分配等方面可能会有一些实质性差异。因此,他们选择与新构造的实现保持一致。
为了比较,下面是我在上述方法中将 T : struct
更改为 T : class
时产生的结果(以及 T?
为 T
):
.method private hidebysig static void C<class T>(object o) cil managed
{
// Code size 47 (0x2f)
.maxstack 3
.locals init (!!T V_0)
IL_0000: ldarg.0
IL_0001: isinst !!T
IL_0006: unbox.any !!T
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: box !!T
IL_0012: brfalse.s IL_002e
IL_0014: ldstr "Argument is {0}: {1}"
IL_0019: ldtoken !!T
IL_001e: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0023: ldloc.0
IL_0024: box !!T
IL_0029: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002e: ret
}
再次与原始方法相当一致。
如果我理解 CLR 装箱和处理可空值的方式,如 Boxing / Unboxing Nullable Types - Why this implementation? 所述,我仍然有一些困惑。例如,下面的 C# 7 代码
void C<T>(object o) where T : struct {
if (o is T t)
Console.WriteLine($"Argument is {typeof(T)}: {t}");
}
编译成如下CIL
IL_0000: ldarg.0
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!!T>
IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<!!T>
IL_000b: stloc.1
IL_000c: ldloca.s 1
IL_000e: call instance !0 valuetype [mscorlib]System.Nullable`1<!!T>::GetValueOrDefault()
IL_0013: stloc.0
IL_0014: ldloca.s 1
IL_0016: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_001b: brfalse.s IL_003c
IL_001d: ldstr "Argument is {0}: {1}"
IL_0022: ldtoken !!T
IL_0027: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_002c: ldloc.0
IL_002d: box !!T
IL_0032: call string [mscorlib]System.String::Format(string, object, object)
IL_0037: call void [mscorlib]System.Console::WriteLine(string)
IL_003c: ret
还有下面的 C#
void D<T>(object o) where T : struct {
if (o is T)
Console.WriteLine($"Argument is {typeof(T)}: {(T) o}");
}
编译成如下CIL
IL_0000: ldarg.0
IL_0001: isinst !!T
IL_0006: brfalse.s IL_002c
IL_0008: ldstr "Argument is {0}: {1}"
IL_000d: ldtoken !!T
IL_0012: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0017: ldarg.0
IL_0018: unbox.any !!T
IL_001d: box !!T
IL_0022: call string [mscorlib]System.String::Format(string, object, object)
IL_0027: call void [mscorlib]System.Console::WriteLine(string)
IL_002c: ret
我认为正在发生的事情:查看第一种方法的CIL,似乎(1)检查参数是否是 [boxed?] Nullable<T>
,如果是,则将其压入堆栈,否则 null
,(2) 将其拆箱(如果是 null
怎么办?),(3) 尝试获取它的值,否则 default(T)
,(4) 然后检查它是否有值,如果没有则分支。第二种方法的 CIL 非常简单,它只是尝试对参数进行拆箱。
如果两种代码的语义相同,为什么前一种情况涉及拆箱到 Nullable<T>
而前一种情况 "just unboxes"?其次,在第一个 CIL 中,如果对象参数是一个盒装 int
,我目前认为这正是它在罐子上所说的(即盒装 int
而不是盒装 Nullable<int>
), isinst
指令不会总是失败吗? Nullable<T>
即使在 CIL 级别也能得到特殊待遇吗?
更新:手写了一些MSIL后,好像object
,如果确实是盒装的int
,可以拆箱成int
或 Nullable<int>
.
.method private static void Foo(object o) cil managed {
.maxstack 1
ldarg.0
isinst int32
brfalse.s L_00
ldarg.0
unbox.any int32
call void [mscorlib]System.Console::WriteLine(int32)
L_00:
ldarg.0
isinst valuetype [mscorlib]System.Nullable`1<int32>
brfalse.s L_01
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
call void [mscorlib]System.Console::WriteLine(int32)
L_01:
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
brtrue.s L_02
ldstr "No value!"
call void [mscorlib]System.Console::WriteLine(string)
L_02:
ret
}
C# 7 中的新语法同时进行类型检查和类型转换。在旧版本中,这通常以两种可能的方式完成。
if(o is T)
//use (T)o
T t = o as T;
if(t != null)
//use t
对于引用类型,第一个有多余的转换,因为 is
被编译为 isinst
和一个条件分支,从你使用的 CIL 指令中可以看出。第二个代码在 CIL 方面与第一个相同,减去额外的 (T)o
转换(编译为 castclass
)。
对于值类型,第二个选项只能用可空类型来完成,我也认为它实际上比第一个慢一些(必须创建一个结构)。
我已经将以下方法编译成CIL:
static void C<T>(object o) where T : struct
{
T? t = o as T?;
if(t != null)
Console.WriteLine("Argument is {0}: {1}", typeof(T), t);
}
生成此代码:
.method private hidebysig static void C<valuetype .ctor ([mscorlib]System.ValueType) T>(object o) cil managed
{
// Code size 48 (0x30)
.maxstack 3
.locals init (valuetype [mscorlib]System.Nullable`1<!!T> V_0)
IL_0000: ldarg.0
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!!T>
IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<!!T>
IL_000b: stloc.0
IL_000c: ldloca.s V_0
IL_000e: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_0013: brfalse.s IL_002f
IL_0015: ldstr "Argument is {0}: {1}"
IL_001a: ldtoken !!T
IL_001f: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0024: ldloc.0
IL_0025: box valuetype [mscorlib]System.Nullable`1<!!T>
IL_002a: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002f: ret
}
这正是问题中的代码,除了调用 GetValueOrDefault,因为我没有获得可空实例的实际值。
可空类型不能直接装箱或拆箱,只能通过其基础值或作为普通空值。第一个 isinst
确保其他类型不会产生异常(我想 isinst !!T
也可以使用),而只是空引用。然后 unbox.any
操作码根据引用形成一个可为空的实例,然后像往常一样使用它。该指令也可以写成 null 检查并自行形成可为 null 的实例,但这样更短。
C# 7 使用第二种方式 is T t
,因此如果 T
是值类型,它除了使用可空类型别无选择。为什么不选择前一个选项?我只能猜测它在语义或实现、变量分配等方面可能会有一些实质性差异。因此,他们选择与新构造的实现保持一致。
为了比较,下面是我在上述方法中将 T : struct
更改为 T : class
时产生的结果(以及 T?
为 T
):
.method private hidebysig static void C<class T>(object o) cil managed
{
// Code size 47 (0x2f)
.maxstack 3
.locals init (!!T V_0)
IL_0000: ldarg.0
IL_0001: isinst !!T
IL_0006: unbox.any !!T
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: box !!T
IL_0012: brfalse.s IL_002e
IL_0014: ldstr "Argument is {0}: {1}"
IL_0019: ldtoken !!T
IL_001e: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0023: ldloc.0
IL_0024: box !!T
IL_0029: call void [mscorlib]System.Console::WriteLine(string,
object,
object)
IL_002e: ret
}
再次与原始方法相当一致。