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,可以拆箱成intNullable<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
}

再次与原始方法相当一致。