在新的c#6"?"的情况下,而不是callvirt空检查

时间:2015-12-30 18:24:03

标签: c# clr roslyn il c#-6.0

考虑到两种方法:

    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不是空的,而是像

new MyClass().MyMethod();

是真的吗?

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

3 个答案:

答案 0 :(得分:8)

M1中的callvirtstandard C# code generation。它提供了语言保证,即永远不能使用空引用调用实例方法。换句话说,它确保p != null并生成NullReferenceException(如果它为null)。你的明确测试不会改变它。

这个保证非常好,如果this为空,则调试NRE非常有用。反而更容易诊断呼叫站点的事故,调试器可以快速向您显示它是p的麻烦制造者。

但当然callvirt不是免费的,虽然成本非常低,但在运行时需要一条额外的处理器指令。因此,如果可以call替换,那么代码将快半个纳秒,给予或接受。它实际上可以与elvis运算符一起使用,因为它已经确保引用不为null,因此C#6编译器利用了它并生成调用而不是callvirt。

答案 1 :(得分:5)

我认为现在很清楚,

  

这是一种在触发事件之前检查null的简单且线程安全的方法。它是线程安全的原因是该功能仅评估左侧一次,并将其保存在临时变量中。 MSDN

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

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

感谢Dan Lyons获取MSDN链接。

答案 2 :(得分:1)

首先使用callvirt代替call这一事实,因为C#规则即使.NET允许,空对象也可能没有调用它们的方法。

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

虽然if (a != null) a.b等是一个常见的习惯用法,但需要进行分析才能发现a在使用b时不能为空。将该分析添加到编译器将需要工作规范,实现,测试和持续测试其他更改引入的回归错误。

a?.b超出了成语,因为它使用了C#必须“知道”的运算符?.。所以C#必须有代码将其转换为空检查,然后是成员访问。因此编译器必须知道在成员访问发生时,a不为空。因此,“知道”使用call的逻辑已经安全。 call来进行额外的分析工作。

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