为什么CallVirt用于在通用类型的Readonly字段上调用方法
请考虑以下情况:
interface ISomething
{
void Call(string arg);
}
sealed class A : ISomething
{
public void Call(string arg) => Console.WriteLine($"A, {arg}");
}
sealed class Caller<T> where T : ISomething
{
private readonly T _something;
public Caller(T something) => _something = something;
public void Call() => _something.Call("test");
}
new Caller<A>(new A()).Call();
对 Caller.Call 的调用及其对 A.Call 的嵌套 tcall 都是通过 callvirt 指令进行的。
但为什么?这两种类型都是众所周知的。除非我误解了什么,否则这里不应该使用 call 而不是 callvirt 吗?
如果是这样 - 为什么不这样做?这仅仅是编译器未完成的优化,还是背后有任何特定原因?
Consider the following:
interface ISomething
{
void Call(string arg);
}
sealed class A : ISomething
{
public void Call(string arg) => Console.WriteLine(quot;A, {arg}");
}
sealed class Caller<T> where T : ISomething
{
private readonly T _something;
public Caller(T something) => _something = something;
public void Call() => _something.Call("test");
}
new Caller<A>(new A()).Call();
Both the call to Caller<A>.Call, as well as its nested tcall to A.Call are lodged through the callvirt instruction.
But why? Both types are exactly known. Unless I'm misunderstanding something, shouldn't it be possible do use call rather than callvirt here?
If so - why is this not done? Is that merely an optimisation not done by the compiler, or is there any specific reason behind this?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
你错过了两件事。
第一个是
callvirt
对接收器进行空检查,而call
则不会。这意味着在null
接收器上使用callvirt
将引发NullReferenceException
,而call
将愉快地调用该方法并传递null
作为第一个参数,这意味着该方法将获取this
参数,该参数为null
。听起来很令人惊讶吗?这是。 IIRC 在非常早期的 .NET 版本中
call
按照您建议的方式使用,人们对this
如何感到非常困惑方法内为 null
。编译器切换到 callvirt 来强制运行时预先进行空检查。编译器只有少数几个地方会发出
调用
:foo?.Method()
,其中Method
是非虚拟的。最后一点特别意味着使方法成为虚拟方法是一个破坏二进制的更改。
只是为了好玩,请参阅此检查
this == nullString.Equals
中的代码>。第二件事是
_something.Call("test");
不是虚拟调用,而是一个受约束的虚拟调用。有一个constrained
opcode 出现在它之前。受约束的虚拟调用是通过泛型引入的。问题是类和结构上的方法调用有点不同:
ldloc
),然后使用call
/callvirt
。ldloc.a
),然后使用call
。对象
上定义的方法,您需要加载结构体值(例如使用ldloc
),将其装箱,然后使用调用
/callvirt
。如果泛型类型不受约束(即它可以是类或结构),编译器不知道该怎么做:应该使用 ldloc 还是 ldloc.a ?应该装箱还是不装箱?
call
还是callvirt
?受约束的虚拟调用将此责任转移到运行时。引用上面的文档:
You're missing two things.
The first is that
callvirt
does a null-check on the receiver, whereascall
does not. This means that usingcallvirt
on anull
receiver will raise aNullReferenceException
, whereascall
will happily call the method and passnull
as the first parameter, meaning that the method will get athis
parameter which isnull
.Sound surprising? It is. IIRC in very early .NET versions
call
was used in the way you suggest, and people got very confused about howthis
could benull
inside a method. The compiler switched tocallvirt
to force the runtime to do a null-check upfront.There are only a handful of places where the compiler will emit a
call
:null
, and we also explicitly do not want to make a virtual call).foo?.Method()
whereMethod
is non-virtual.That last point in particular means that making a method
virtual
is a binary-breaking change.Just for fun, see this check for
this == null
inString.Equals
.The second thing is that
_something.Call("test");
is not a virtual call, it's a constrained virtual call. There's aconstrained
opcode which appears before it.Constrained virtual calls were introduced with generics. The problem is that method calls on classes and on structs are a bit different:
ldloc
), then usecall
/callvirt
.ldloc.a
), then usecall
.object
, you need to load the struct value (e.g. withldloc
), box it, then usecall
/callvirt
.If a generic type is unconstrained (i.e. it could be a class or a struct), the compiler doesn't know what to do: should it use
ldloc
orldloc.a
? Should it box or not?call
orcallvirt
?Constrained virtual calls move this responsibility to the runtime. To quote the doc above: