为什么CallVirt用于在通用类型的Readonly字段上调用方法

发布于 2025-01-17 14:36:02 字数 627 浏览 4 评论 0原文

请考虑以下情况:

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

不回头走下去 2025-01-24 14:36:02

你错过了两件事。

第一个是 callvirt 对接收器进行空检查,而 call 则不会。这意味着在 null 接收器上使用 callvirt 将引发 NullReferenceException,而 call 将愉快地调用该方法并传递 null 作为第一个参数,这意味着该方法将获取 this 参数,该参数为 null

听起来很令人惊讶吗?这是。 IIRC 在非常早期的 .NET 版本中call按照您建议的方式使用,人们对this如何感到非常困惑方法内为 null。编译器切换到 callvirt 来强制运行时预先进行空检查。

编译器只有少数几个地方会发出调用

  1. 静态方法。
  2. 非虚拟结构方法。
  3. 调用基本方法或基本构造函数(我们知道接收者不为 null,并且我们也明确不想进行虚拟调用)。
  4. 编译器确定接收者不为 null,例如 foo?.Method(),其中 Method 是非虚拟的。

最后一点特别意味着使方法成为虚拟方法是一个破坏二进制的更改。

只是为了好玩,请参阅此检查 this == nullString.Equals 中的代码>


第二件事是 _something.Call("test"); 不是虚拟调用,而是一个受约束的虚拟调用。有一个 constrained opcode 出现在它之前。

受约束的虚拟调用是通过泛型引入的。问题是类和结构上的方法调用有点不同:

  1. 对于类,您加载类引用(例如使用ldloc),然后使用call / callvirt
  2. 对于结构,您加载结构的地址(例如使用ldloc.a),然后使用call
  3. 要调用结构体上的接口方法或对象上定义的方法,您需要加载结构体值(例如使用ldloc),将其装箱,然后使用调用 / callvirt

如果泛型类型不受约束(即它可以是类或结构),编译器不知道该怎么做:应该使用 ldloc 还是 ldloc.a ?应该装箱还是不装箱? call 还是 callvirt

受约束的虚拟调用将此责任转移到运行时。引用上面的文档:

callvirt method 指令带有 constrained thisType 前缀时,指令执行如下:

  • 如果 thisType 是引用类型(而不是值类型),则 ptr 被取消引用并作为“this”指针传递给 callvirt< 方法的/code>。
  • 如果 thisType 是值类型,并且 thisType 实现 method,则 ptr 未经修改地作为“this”传递' 指向callmethod指令的指针,用于通过thisType实现method
  • 如果 thisType 是值类型,并且 thisType 未实现 method,则 ptr 将被取消引用、装箱,并作为“this”指针传递给 callvirt method 指令。

仅当在 System.ObjectSystem.ValueTypeSystem.Enum 上定义 method 时,才会出现最后一种情况 并且不会被 thisType 覆盖。在这种情况下,装箱会导致生成原始对象的副本。但是,由于 System.ObjectSystem.ValueTypeSystem.Enum 的方法都不会修改对象的状态,因此这一事实无法检测到。

You're missing two things.

The first is that callvirt does a null-check on the receiver, whereas call does not. This means that using callvirt on a null receiver will raise a NullReferenceException, whereas call will happily call the method and pass null as the first parameter, meaning that the method will get a this parameter which is null.

Sound surprising? It is. IIRC in very early .NET versions call was used in the way you suggest, and people got very confused about how this could be null inside a method. The compiler switched to callvirt to force the runtime to do a null-check upfront.

There are only a handful of places where the compiler will emit a call:

  1. Static methods.
  2. Non-virtual struct methods.
  3. Calling a base method or base constructor (where we know the receiver is not null, and we also explicitly do not want to make a virtual call).
  4. Where the compiler is certain that the receiver is not null, e.g. foo?.Method() where Method 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 in String.Equals.


The second thing is that _something.Call("test"); is not a virtual call, it's a constrained virtual call. There's a constrained 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:

  1. For classes, you load the class reference (e.g. with ldloc), then use call / callvirt .
  2. For structs, you load the address of the struct (e.g. with ldloc.a), then use call.
  3. To call an interface method on a struct, or a method defined on object, you need to load the struct value (e.g. with ldloc), box it, then use call / 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 or ldloc.a? Should it box or not? call or callvirt?

Constrained virtual calls move this responsibility to the runtime. To quote the doc above:

When a callvirt method instruction has been prefixed by constrained thisType, the instruction is executed as follows:

  • If thisType is a reference type (as opposed to a value type) then ptr is dereferenced and passed as the 'this' pointer to the callvirt of method.
  • If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.
  • If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.

This last case can occur only when method was defined on System.Object, System.ValueType, or System.Enum and not overridden by thisType. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods of System.Object, System.ValueType, and System.Enum modify the state of the object, this fact cannot be detected.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文