为什么 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
:
- 静态方法。
- Non-virtual 结构方法。
- 调用基本方法或基本构造函数(我们知道接收者不是
null
,并且我们也明确不想进行虚拟调用)。
- 编译器确定接收者不为空的地方,例如
foo?.Method()
其中 Method
是 non-virtual.
最后一点特别意味着制作方法 virtual
是 binary-breaking 更改。
纯属娱乐,参见String.Equals
中的this check for this == null
。
第二件事是 _something.Call("test");
不是虚拟调用,它是 constrained 虚拟调用。在它之前出现了一个constrained
opcode。
约束虚拟调用是通过泛型引入的。问题是 classes 和结构上的方法调用有点不同:
- 对于 classes,加载 class 引用(例如使用
ldloc
),然后使用 call
/ callvirt
.
- 对于结构,加载结构的地址(例如使用
ldloc.a
),然后使用call
.
- 要调用结构上的接口方法,或在
object
上定义的方法,您需要加载结构值(例如使用 ldloc
),将其装箱,然后使用 call
/ callvirt
.
如果泛型类型不受约束(即它可以是 class 或结构),编译器不知道该怎么做:它应该使用 ldloc
还是 ldloc.a
?它应该装箱还是不装箱? call
或 callvirt
?
受约束的虚拟调用将此责任转移到运行时。引用上面的文档:
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.
考虑以下几点:
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
:
- 静态方法。
- Non-virtual 结构方法。
- 调用基本方法或基本构造函数(我们知道接收者不是
null
,并且我们也明确不想进行虚拟调用)。 - 编译器确定接收者不为空的地方,例如
foo?.Method()
其中Method
是 non-virtual.
最后一点特别意味着制作方法 virtual
是 binary-breaking 更改。
纯属娱乐,参见String.Equals
中的this check for this == null
。
第二件事是 _something.Call("test");
不是虚拟调用,它是 constrained 虚拟调用。在它之前出现了一个constrained
opcode。
约束虚拟调用是通过泛型引入的。问题是 classes 和结构上的方法调用有点不同:
- 对于 classes,加载 class 引用(例如使用
ldloc
),然后使用call
/callvirt
. - 对于结构,加载结构的地址(例如使用
ldloc.a
),然后使用call
. - 要调用结构上的接口方法,或在
object
上定义的方法,您需要加载结构值(例如使用ldloc
),将其装箱,然后使用call
/callvirt
.
如果泛型类型不受约束(即它可以是 class 或结构),编译器不知道该怎么做:它应该使用 ldloc
还是 ldloc.a
?它应该装箱还是不装箱? call
或 callvirt
?
受约束的虚拟调用将此责任转移到运行时。引用上面的文档:
When a
callvirt
method
instruction has been prefixed byconstrained
thisType
, the instruction is executed as follows:
- If
thisType
is a reference type (as opposed to a value type) thenptr
is dereferenced and passed as the 'this' pointer to thecallvirt
ofmethod
.- If
thisType
is a value type andthisType
implementsmethod
thenptr
is passed unmodified as the 'this' pointer to acall
method
instruction, for the implementation ofmethod
bythisType
.- If
thisType
is a value type andthisType
does not implementmethod
thenptr
is dereferenced, boxed, and passed as the 'this' pointer to thecallvirt
method
instruction.This last case can occur only when
method
was defined onSystem.Object
,System.ValueType
, orSystem.Enum
and not overridden bythisType
. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods ofSystem.Object
,System.ValueType
, andSystem.Enum
modify the state of the object, this fact cannot be detected.