如果新的c#6“?”null检查,则调用而不是callvirt
鉴于这两种方法:
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,该怎么办?
M1中的callvirt
是标准的C#代码生成 。 它提供了语言保证,即永远不能使用空引用调用实例方法。 换句话说,它确保p != null
并且如果它为null则生成NullReferenceException。 您的明确测试不会改变这一点。
这个保证非常好,调试NRE非常多毛,如果它是null。 反而更容易诊断呼叫站点的事故,调试器可以快速向您显示它是麻烦制造者。
但是,当然, callvirt
不是免费的,虽然成本很低,但在运行时需要额外的一条处理器指令。 因此,如果它可以被call
替换,则代码将更快半个纳秒,给予或接受。 它实际上可以与elvis运算符一起使用,因为它已经确保引用不为null,因此C#6编译器利用了它并生成调用而不是callvirt。
我认为现在很清楚,
在触发事件之前,这是一种检查null的简单且线程安全的方法。 它是线程安全的原因是该function仅评估左侧一次,并将其保存在临时变量中。 MSDN
所以在这里使用call
指令是安全的。
我写了一篇关于call
和callvirt
之间callvirt
以及为什么C#生成callvirt
的博客文章
感谢Dan Lyons的MSDN链接。
首先使用callvirt
而不是call
因为C#规则,即使.NET允许,null对象也可能没有调用它们的方法。
现在,在你的两个方法中,我们可以静态地显示p
不是null,因此使用call
而不是callvirt
不会破坏这个C#规则,因此这是一个合理的优化。
虽然if (a != null) ab
等是常见的习语,但需要分析才能认识到a
在使用b
不能为空。 将该分析添加到编译器将需要工作规范,实现,测试和持续测试其他更改引入的回归错误。
a?.b
超出了成语,因为它使用的是运算符?.
C#必须“知道”。 所以C#必须有代码将其转换为空检查,然后是成员访问。 因此编译器必须知道在成员访问发生的时刻, a
不是null。 因此,“知道”使用call
的逻辑是安全的已经完成。 没有额外的分析工作来实现可以使用call
。
因此,第一种情况需要一些额外的工作来使用call
并可能引入错误,而第二种情况无论如何都必须完成这项工作,所以它也可能。