4

考虑这个 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's ) ,(根据 CLI 规范的第 I.8.9.7 节) 根本没有基本类型?ToString

我目前的不完全理解是这样的:

  • ldloca.s foo*产生一个指向局部变量foo(它包含一个未装箱的类型的值)的临时指针( ) valuetype Foo,在这种情况下,根据 CLI 规范的第 I.12.3.2.1 节,可以在需要托管指针(&)的地方使用。

  • *指针将充当方法调用的this指针。这似乎是合法的,因为它可以在此处充当托管指针 ( &)。CLI 标准在第 I.8.9.7 节中提到了这种可能性。

  • constrained. Foo前缀是为了防止将值装箱到valuetype Foo对象boxed Foo中。

但主要问题仍然存在:为什么可以在未继承该虚拟方法的未装箱值上调用虚拟方法?

4

1 回答 1

4

System.Object.ToString一个未装箱的值类型(根据 CLI 规范的第 I.8.9.7 节)根本没有基本类型,怎么可能调用虚拟方法?

我对这个问题感到困惑。有或没有基本类型与它有什么关系?

我想知道这三行到底发生了什么

关键是constrained前缀。文档——Partition III 第 2.1 节——非常简单。在文档中,我们有一个接收器类型thisType、一个指向该类型的托管指针ptr和一个constrained.callvirtof method。规则是:

  1. 如果thisType是引用类型,则ptr取消引用并作为指向of的this指针传递callvirtmethod
  2. IfthisType是一个值类型并thisType实现methodthen未经ptr修改地作为this指向 acall的指针传递methodthisType
  3. IfthisType是一个值类型并且thisType没有实现方法 then ptr被取消引用,装箱,并作为this指针传递给callvirtofmethod

在您的示例中,第 (3) 点适用。该类型Foo是一个值类型,它没有实现方法ToString,所以它被装箱并且方法(由基类提供)被调用时引用该框作为this

假设我们有 int.ToString。然后第 (2) 点适用。类型是int,它是一个值类型,并int实现了对 的覆盖System.Object.ToString()。因此,指向 的托管指针int成为this调用的ToString。从而消除了不必要的拳击。(如果ToString发生突变,int那么突变将发生在作为接收者给出的变量上,而不是盒装副本上。)

为什么可以在未继承该虚拟方法的未装箱值上调用虚拟方法?

手头的问题是该方法是否已实现,正如我在上面引用的文档中所指出的那样。继承与它有什么关系?

一个你没有问的问题,但这是一个回答它的好地方:

我应该总是在我的值类型上实现 ToString 吗?

好吧,我不知道always,但是这样做当然是个好主意,因为(1)默认实现ToString是令人沮丧的,并且(2)通过在值类型上实现它,您可以设法消除装箱惩罚任何时候直接调用该方法。

对象的其他虚拟方法也一样吗?

是的。无论如何,有充分的理由在值类型中创建自己的相等和散列。默认值类型相等有时可能出乎意料。

我注意到 GetType 不是虚拟的。这有关系吗?

是的; 不是虚拟的意味着它不能在值类型中被覆盖,这意味着调用GetType任何值类型总是将它装箱。当然,如果您手头有一个未装箱的值类型,那么您不需要调用GetType,因为您在编译时已经知道它的类型是什么!

于 2013-02-22T22:26:03.700 回答