请原谅我的冗长帖子,但这个主题相当广泛。我将尝试描述 C# 编译器发出的内容以及 JIT 编译器在运行时如何解释这些内容。
ECMA-335(这是一个写得很好的设计文档;检查一下)是了解一切(我的意思是一切)如何在 .NET 程序集中表示的地方。程序集中的通用信息有一些相关的 CLI 元数据表:
- GenericParam - 存储有关通用参数的信息(索引、标志、名称、拥有类型/方法)。
- GenericParamConstraint - 存储有关泛型参数约束(拥有泛型参数、约束类型)的信息。
- MethodSpec - 存储实例化的通用方法签名(例如 Bar.Method 用于 Bar.Method)。
- TypeSpec - 存储实例化的泛型类型签名(例如 Bar for Bar)。
考虑到这一点,让我们通过一个使用此类的简单示例:
class Foo<T>
{
public T SomeProperty { get; set; }
}
当 C# 编译器编译此示例时,它将在 TypeDef 元数据表中定义 Foo ,就像对任何其他类型一样。与非泛型类型不同,它在 GenericParam 表中也有一个条目,用于描述其泛型参数(索引 = 0,标志 = ?,名称 =(字符串堆的索引,“T”),所有者 = 类型“Foo ")。
TypeDef 表中的一列数据是 MethodDef 表的起始索引,它是在此类型上定义的方法的连续列表。对于 Foo,我们定义了三个方法:SomeProperty 的 getter 和 setter,以及编译器提供的默认构造函数。因此,MethodDef 表将为这些方法中的每一个保留一行。 MethodDef 表中的重要列之一是“签名”列。此列存储对描述该方法的确切签名的字节块的引用。 ECMA-335 非常详细地介绍了这些元数据签名 blob,因此我不会在这里重复这些信息。
方法签名 blob 包含有关参数的类型信息以及返回值。在我们的示例中,setter 接受一个 T,而 getter 返回一个 T。那么,什么是 T?在签名 blob 中,它将是一个特殊值,表示“索引 0 处的泛型类型参数”。这意味着 GenericParams 表中 index=0 且 owner=type "Foo" 的行,即我们的 "T"。
自动属性后备存储字段也是如此。 Foo 在 TypeDef 表中的条目将有一个到 Field 表的起始索引,并且 Field 表有一个“签名”列。该字段的签名将表示该字段的类型是“索引 0 处的泛型类型参数”。
这一切都很好,但是当 T 是不同的类型时,代码生成在哪里发挥作用?实际上,JIT 编译器负责为通用实例生成代码,而不是 C# 编译器。
我们来看一个例子:
Foo<int> f1 = new Foo<int>();
f1.SomeProperty = 10;
Foo<string> f2 = new Foo<string>();
f2.SomeProperty = "hello";
这将编译成类似这样的 CIL:
newobj <MemberRefToken1> // new Foo<int>()
stloc.0 // Store in local "f1"
ldloc.0 // Load local "f1"
ldc.i4.s 10 // Load a constant 32-bit integer with value 10
callvirt <MemberRefToken2> // Call f1.set_SomeProperty(10)
newobj <MemberRefToken3> // new Foo<string>()
stloc.1 // Store in local "f2"
ldloc.1 // Load local "f2"
ldstr <StringToken> // Load "hello" (which is in the user string heap)
callvirt <MemberRefToken4> // Call f2.set_SomeProperty("hello")
那么这个 MemberRefToken 业务是什么? MemberRefToken 是一个元数据标记(标记是四个字节值,最高有效字节是元数据表标识符,其余三个字节是行号,从 1 开始)引用 MemberRef 元数据表中的一行。此表存储对方法或字段的引用。在泛型之前,此表将存储有关您正在使用的方法/字段的信息,这些信息来自引用程序集中定义的类型。但是,它也可以用于引用泛型实例化的成员。因此,假设 MemberRefToken1 指的是 MemberRef 表中的第一行。它可能包含以下数据:class= TypeSpecToken1,name = ".ctor",blob = 。
TypeSpecToken1 将引用 TypeSpec 表中的第一行。从上面我们知道这个表存储了泛型类型的实例。在这种情况下,该行将包含对“Foo”的签名 blob 的引用。所以这个 MemberRefToken1 确实是在说我们正在引用“Foo.ctor()”。
MemberRefToken1 和 MemberRefToken2 将共享相同的类值,即 TypeSpecToken1。但是,它们在名称和签名 blob 上会有所不同(MethodRefToken2 将用于“set_SomeProperty”)。同样,MemberRefToken3 和 MemberRefToken4 将共享 TypeSpecToken2,即 "Foo" 的实例化,但名称和 blob 相同方式。
当 JIT 编译器编译上述 CIL 时,它注意到它看到了一个以前没有见过的通用实例(即 Foo 或 Foo)。 Shiv Kumar 的回答很好地涵盖了接下来发生的事情,所以我不会在这里详细重复。简而言之,当 JIT 编译器遇到一个新的实例化泛型类型时,它可能会在其类型系统中发出一个全新的类型,其字段布局使用实例化中的实际类型代替泛型参数。他们还将拥有自己的方法表,并且每个方法的 JIT 编译将涉及用实例化的实际类型替换对泛型参数的引用。 JIT 编译器还有责任强制执行 CIL 的正确性和可验证性。
总结一下:C# 编译器发出元数据,描述什么是泛型以及泛型类型/方法是如何实例化的。 JIT 编译器使用此信息在运行时为实例化的泛型类型生成新类型(假设它与现有实例化不兼容),并且每种类型都将拥有自己的代码副本,该副本已根据使用的实际类型进行 JIT 编译在实例化中。
希望这在某种程度上是有意义的。