考虑这个IL片段(由Microsoft的C#编译器生成):
.class public sequential ansi sealed beforefieldinit Foo
extends [mscorlib]System.ValueType
{ … }
.method private hidebysig static void Main(string[] args) cil managed
{
.maxstack 1
.locals init ([0] valuetype Foo foo)
ldloca.s foo // ?
constrained. Foo // ?
callvirt instance string [mscorlib]System.Object::ToString() // ?
pop
ret
}
我想确切地知道标记为// ?
的三行中发生了什么:如何调用虚拟方法(System.Object
的{{1}}) unboxed 值类型,(根据CLI规范的I.8.9.7节)根本没有基类型?
我目前的不完全理解是:
ToString
会导致局部变量ldloca.s foo
的瞬态指针(*
)(其中包含类型foo
的未装箱值),在这种情况下根据CLI规范的第I.12.3.2.1节,可以在需要托管指针(valuetype Foo
)的地方使用。
此&
指针将充当方法调用的 this 指针。这似乎是合法的,因为它可以在此处充当托管指针(*
)。 CLI标准在第I.8.9.7节中提到了这种可能性。
&
前缀用于防止将constrained. Foo
值绑定到valuetype Foo
对象。
但主要问题仍然存在:为什么可以在未继承该虚方法的未装箱值上调用虚方法?
答案 0 :(得分:3)
如何在未装箱的值类型上调用虚方法
System.Object.ToString
,(根据CLI规范的I.8.9.7节)根本没有基类型?
我对这个问题感到困惑。拥有或不拥有基本类型的内容与它有什么关系?
我想知道三行中到底发生了什么
密钥是constrained
前缀。文档 - 分区III第2.1节 - 非常简单。在文档中,我们有一种接收器thisType
,一种指向该类型ptr
的托管指针,以及constrained.callvirt
method
。规则是:
- 如果
的thisType
是引用类型,则ptr
将被取消引用,并作为this
指针传递给callvirt
method
- 如果
实施的thisType
是值类型且thisType
实现method
,那么ptr
将作为this
指针未经修改地传递给call
由method
thisType
- 如果
的thisType
是值类型而thisType
没有实现方法ptr
被取消引用,装箱,并作为this
指针传递到callvirt
method
醇>
在你的例子中,第(3)点适用。类型Foo
是一个值类型,它不实现方法ToString
,因此它被加框并且方法(由基类提供)被调用,并引用该框作为{{1 }}
假设我们有int.ToString。然后点(2)适用。类型为this
,它是值类型,int
实现了int
的覆盖。因此,System.Object.ToString()
的托管指针成为int
调用的this
。从而省略了不必要的拳击。 (如果ToString
突变ToString
,则变异将发生在作为接收者给出的变量上,而不是在盒装副本上。)
为什么可以在未继承该虚方法的未装箱值上调用虚方法?
目前的问题是该方法是否实施,正如我在上面引用的文档中所述。继承与它有什么关系?
你没有问的问题,但这是一个很好的答案:
我应该总是在我的值类型上实现ToString吗?
好吧,我不知道总是,但这样做肯定是个好主意,因为(1)int
的默认实现是令人沮丧的,(2)通过在您的值类型上实现它,您可以设法在任何直接调用方法时消除拳击惩罚。
对于其他虚拟对象方法是否也一样?
是的。并且有充分的理由无论如何都要在值类型中进行自己的平等和散列。默认值类型相等有时可能是意外的。
我注意到GetType不是虚拟的。这有关系吗?
是;不是虚拟意味着它不能在值类型中被覆盖,这意味着在任何值类型上调用ToString
总是将其打包。当然,如果您手头有未装箱的值类型,那么您不需要调用GetType
,因为您已经知道它在编译时的类型是什么!