CLR引用究竟是什么,它如何保存类型信息?

4

今天早上我脑子短路了,试图理解C#如何以及何时可以通过对对象的引用来确定对象的类型。考虑以下非常不原创的示例代码:

class Foo { public virtual void Baz() { } }
class Bar : Foo { }

class Program {
    static void Main() {
        Foo f = new Bar();
        f.Baz();
    }
}

引用类型是Foo,但实际创建的对象实例是Bar。该Bar实例有一些开销,例如同步块索引和对一个方法表的引用,可能是Bar的方法表。如果您查看堆上的Bar对象,其类型的唯一线索是方法表引用,这表明它是一个Bar。

那么问题来了。C#有没有办法从实际对象图中知道"f"是Foo,如果有,如何知道?参考"f"本身是否包含类型信息?当我调用f.Baz()时,我想知道分派是否仍然通过Bar的方法表进行?C#编译器是否仅使用流分析来解决正在发生的事情并防止任何非法操作?当Foo声明被翻译成IL后,CLR实际上不再关心吗?

如果这是一个冗长且措辞不佳的问题,请原谅 - 让我知道是否需要任何澄清!

TL;DR- CLR中多态引用是如何工作的?实际与声明的类类型之间的差异如何持久化,并且能否从生成的IL中推断出原始声明是什么?

2个回答

5

你想得太复杂了。

参考文献'f'本身包含类型信息吗?

不,它不必包含。它只是一个地址,指向先前构造的Bar对象内存的开头。那个对象包含一个虚拟方法表(可能还有与其相关联的Type对象的引用1,但这在这里无关紧要)。

当我调用f.Baz()时,我是否正确地认为分派仍然通过Bar的MethodTable进行?

是的。

只是C#编译器使用流分析来确定正在发生什么并防止任何非法操作吗?

流分析很复杂,在这里完全没有必要。编译器允许那些声明类型为f的操作 - 即Foo。编译器根本不关心f的实际(=动态)类型。

您能否从结果IL中告诉原始声明是什么?

取决于情况。对象没有静态类型,因此“告诉其运行时静态类型”是没有意义的。另一方面,声明不同。如果变量是方法的形式参数,则可以(在运行时)使用反射来确定方法参数的声明类型。

对于局部变量,这个操作再次没有意义。另一方面,IL通过.locals存储此信息(作为元数据?),因此可以理论上反向工程化代码(Cecil和Reflector会这样做)以获取变量的静态类型。


1 我猜测这里,但实际上这是不太可能的。如果每个对象都持有其关联的Type对象的引用,那么这将意味着指针的额外开销。此外,这个引用是完全不必要的,因为对象可以简单地调用GetType来获取其类型。 GetType只需要为每个类实现一次(实际上是一种静态方法),并通过通常的虚拟函数表分派。因此,每个类只需要一个对Type的引用,而不是每个对象。


非常有帮助,谢谢。您能详细解释一下“对象没有静态类型”的评论吗?我认为堆上的给定具体化对象由MethodTable引用指向的确定静态类型。不过,总的来说似乎声明的类型只对编译器有影响,而CLR只知道编译器是否在IL中留下了某种元数据标记。这听起来正确吗? - Jon Artus
1
@Jon:这是它的动态类型。静态类型仅分配给声明,即代码中的标识符。由于两个(不同静态类型)变量可以指向同一对象,因此对象不能具有静态类型(尽管这过于简化)。 - Konrad Rudolph
噢,我之前没有意识到这一点,但是现在你提到了,它完全合理。我现在怀疑我尝试查找的更好的措辞可能是:“CLR 中的引用是否包含静态类型信息”。 - Jon Artus

1
C# 是否有办法从实际对象图中知道 'f' 是一个 Foo 呢?这在编译器中是静态已知的。
参考 'f' 包含类型信息本身吗?有趣,它一定以某种方式包含在 IL 中。
当我调用 f.Baz() 时,我是否正确地认为分派始终通过 Bar 的 MethodTable 发生?是的。并且是通过 f 指向的实例找到的。

谢谢你的回答 - “有趣,它一定以某种方式包含在IL中。”这部分确实很有趣。你知道这是如何发生的吗?在我的示例代码中会发生吗?也就是说,在调用f.Baz()的地方,我能否从IL或检查对象图表中得知f实际上被声明为Foo? - Jon Artus
你不可能不知道 'f' 是一个 Foo 就使用它... 引用类型很像值类型,你怎么可能不知道 'i' 是一个 int 就使用它呢? - H H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接