在新的 c# 6“?”的情况下调用而不是 callvirt无效检查

call instead of callvirt in case of the new c# 6 "?" null check

鉴于两种方法:

    static void M1(Person p)
    {
        if (p != null)
        {
            var p1 = p.Name;
        }
    }

    static void M2(Person p)
    {
        var p1 = p?.Name;
    }

为什么M1 IL代码使用callvirt:

IL_0007:  brfalse.s  IL_0012
IL_0009:  nop
IL_000a:  ldarg.0
IL_000b:  callvirt   instance string ConsoleApplication4.Person::get_Name()

M2 IL 使用call:

brtrue.s   IL_0007
IL_0004:  ldnull
IL_0005:  br.s       IL_000d
IL_0007:  ldarg.0
IL_0008:  call       instance string ConsoleApplication4.Person::get_Name()

我只能猜到,因为在 M2 中我们知道 p 不是 null 之类的

new MyClass().MyMethod();

这是真的吗?

如果是,如果p在其他线程中为null怎么办?

我想现在很清楚了,

This is an easy and thread-safe way to check for null before you trigger an event. The reason it’s thread-safe is that the feature evaluates the left-hand side only once, and keeps it in a temporary variable. MSDN

所以在这里使用call指令是安全的。

我写了一个 blog post 关于 callcallvirt 之间的差异以及为什么 C# 生成 callvirt

感谢 Dan Lyons 提供 MSDN link。

M1中的callvirtstandard C# code generation。它提供了永远不能用空引用调用实例方法的语言保证。换句话说,它确保 p != null 并在它为 null 时生成 NullReferenceException。您的显式测试不会改变这一点。

这个保证非常好,如果 this 为 null,则调试 NRE 会变得很棘手。相反,诊断 call-site 处的错误要容易得多,调试器可以快速向您显示 p 才是麻烦制造者。

当然callvirt也不是免费的,虽然成本很低,运行时多一条处理器指令。因此,如果它 可以 替换为 call 那么代码将快半纳秒,给或取。它实际上可以与 elvis 运算符一起使用,因为它已经确保引用不为空,因此 C# 6 编译器利用了这一点并生成调用而不是 callvirt。

首先使用 callvirt 而不是 call,因为 C# 规则规定 null 对象不能调用它们的方法,即使 .NET 允许也是如此。

现在,在您的两种方法中,我们可以静态地显示 p 不为空,因此使用 call 而不是 callvirt 不会破坏此 C# 规则,因此是一个合理的优化。

虽然if (a != null) a.b等是常用的成语,但需要分析才能发现a在使用b的地方不能为null。将该分析添加到编译器将需要针对其他更改引入的回归错误进行规范、实施、测试和持续测试。

a?.b 超出了惯用法,因为它使用了 C# 必须 "know" 的运算符 ?.。所以 C# 必须有代码将其转换为空检查,然后是成员访问。所以编译器必须知道在成员访问发生的地方,a 不为空。因此,"know" 使用 call 是安全的逻辑已经完成。意识到可以使用call不需要额外的分析工作。

所以第一种情况需要大量额外的工作才能使用 call 并且可能会引入错误,而第二种情况无论如何都必须完成这项工作,所以它也可以。