我怀疑这两个方面;
第一个;
Test test = new Test();
result = test.DoWork(_param);
第二个;
result = new Test().DoWork(_param);
如果我们不将新创建的实例分配给变量并直接调用方法会发生什么?
我看到IL代码的两种方式有所不同。
下面这个是第一个c#代码的IL输出
IL_0000: ldstr "job "
IL_0005: stloc.0
IL_0006: newobj instance void Works.Test::.ctor()
IL_000b: stloc.1
IL_000c: ldloc.1
IL_000d: ldloc.0
IL_000e: callvirt instance string Works.Test::DoWork(string)
IL_0013: pop
IL_0014: ret
这是第二个c#代码
的IL输出 IL_0000: ldstr "job "
IL_0005: stloc.0
IL_0006: newobj instance void Works.Test::.ctor()
IL_000b: ldloc.0
IL_000c: call instance string Works.Test::DoWork(string)
IL_0011: pop
IL_0012: ret
你能告诉我吗?
答案 0 :(得分:20)
这个问题有点难以找到,但我想你要问的是:
为什么将新创建的引用分配给变量会导致编译器生成callvirt,但调用该方法会直接生成调用?
你非常敏锐地注意到这种微妙的差异。
在我们回答您的问题之前,请回答其他一些问题。
我是否应该相信编译器会生成良好的代码?
一般是的。偶尔有代码生成错误,但这不是其中之一。
使用callvirt调用非虚方法是否合法?
是
通过调用调用虚方法是否合法?
是的,如果您尝试调用基类方法而不是派生类中的覆盖。但通常这种情况不会发生。
此示例中的方法是否为虚拟调用?
它不是虚拟的。
由于该方法不是虚拟的,因此可以使用callvirt或call调用。为什么编译器有时会生成callvirt并且有时会生成call,当它可以生成callvirt时,或者同时调用两次?
现在我们来看你问题的有趣部分。
call和callvirt有两个不同之处。
呼叫不做虚拟调度; callvirt在调用它之前在虚函数调度表中查找正确的方法。因此,callvirt的速度要慢一个纳秒。
callvirt 始终检查接收方是否为空,无论所调用的方法是否为虚拟。 call不检查接收器是否为空。使用null"这个"调用方法是合法的。通过电话。
现在也许你会看到它的发展方向。
每当在空引用接收器上进行调用时,C#是否需要以空解除引用异常崩溃?
是即可。当您使用null接收器调用某些内容时,C# required 会崩溃。因此,在生成调用方法的代码时,C#有以下选择:
案例1只是愚蠢。 IL更大,因此在磁盘上占用更多空间,加载速度更慢,jit更慢。当callvirt自动进行空检查时,生成此代码是愚蠢的。
案例2很聪明。 C#编译器生成callvirts,以便自动完成null检查。
那么案例3呢?在什么情况下C#可以跳过空检查并生成一个调用?只有在:
但是C#知道在new Foo().Bar()
中,接收者不能为空,因为如果是,那么构造会引发异常,我们永远不会接到电话!
编译器不足够智能,以便意识到变量只被分配了非空值。所以它会生成一个安全的callvirt。
编译器可以编写为智能。编译器必须跟踪变量的赋值状态以进行明确的赋值检查。它还可以跟踪"被分配的东西可能是空的"状态,然后它会在两种情况下生成一个调用。但是编译器还不是那么聪明。
答案 1 :(得分:0)
如果你不做任何其他事情,两种做法之间没有明显的区别。
唯一真正的区别在于,在第一种情况下,您将Test
实例分配给变量,以便稍后可以访问它/在调试器中检查它。在第二种情况下,你不能这样做。
就逻辑而言,如果你以后不再对test
做任何事情,除了第二种情况下的一个非常小的性能改进之外没有任何区别(这么小我无法想到任何实际的它可能会计数的情况。)