为什么 callvirt 用于在泛型类型的只读字段上调用方法

Why is callvirt used to call a method on a readonly field of generic type

考虑以下几点:

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 吗?

如果是这样 - 为什么不这样做?这仅仅是编译器没有进行的优化,还是背后有任何特定原因?

你错过了两件事。

首先是 callvirt 在接收器上执行 null-check,而 call 没有。这意味着在 null 接收器上使用 callvirt 将引发 NullReferenceException,而 call 将愉快地调用该方法并将 null 作为第一个参数传递,这意味着该方法将获得一个 this 参数,即 null.

听起来令人惊讶?这是。 IIRC 在非常早期的 .NET 版本中 call 按照你建议的方式使用的,人们对 this 如何在里面 null 感到非常困惑一个方法。编译器切换到 callvirt 以强制运行时预先执行 null-check。

只有少数地方编译器会发出 call:

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

最后一点特别意味着制作方法 virtual 是 binary-breaking 更改。

纯属娱乐,参见String.Equals中的this check for this == null


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

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

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

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

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

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.