什么是NET公共语言运行时的泛型实现

问题描述:

当你在C#中使用泛型集合(或者一般的.NET)时,编译器基本上是做腿脚工作的开发者过去必须做的泛型集合为特定类型。所以基本上。 。 。它只是节省我们的工作?什么是NET公共语言运行时的泛型实现

现在我想到了,那是不对的。因为没有泛型,我们过去必须使内部使用非泛型数组的集合,所以有拳击和拆箱(如果它是一个值类型的集合)等。

那么,泛型如何呈现在CIL中?当我们说我们想要一个通用的东西集合时,它在做什么?我不一定需要CIL代码示例(虽然那样可以),但我想知道编译器如何使用我们的泛型集合并呈现它们的概念。

谢谢!

P.S.我知道我可以用ildasm来看这个,但是我的CIL对我来说仍然看起来像中文,我还没有准备好解决这个问题。我只想了解C#(以及其他语言,我猜是如何)在CIL中处理泛型的概念。

+1

您的问题不是特定于集合。所有泛型以相同的方式工作,无论它们是否是泛型集合。 – 2011-02-24 03:31:51

+0

好的。我不确定。有人可以编辑问题标题吗? – richard 2011-02-24 03:34:30

+0

今天早些时候的这个问题有很多链接到回答你的问题的文章。 http://stackoverflow.com/questions/5098335/how-generics-influenced-the-design-of-c-and-net – 2011-02-24 05:26:58

原谅我冗长的文章,但这个话题是非常广泛的。我将尝试描述C#编译器发出的内容以及JIT编译器在运行时解释的内容。 (这是一本写得很好的设计文档;查看它)是了解.NET程序集中如何表达所有内容以及我的意思的地方。有一些相关的CLI元数据表用于装配中的通用信息:

  1. GenericParam - 存储有关通用参数(索引,标志,名称,拥有类型/方法)的信息。
  2. GenericParamConstraint - 存储有关通用参数约束(拥有通用参数,约束类型)的信息。
  3. MethodSpec - 存储实例化的通用方法签名(例如Bar.Method <int> for Bar.Method <T>)。
  4. TypeSpec - 存储实例化的通用类型签名(例如,Bar <int> for Bar <T>)。
考虑到这一点

所以,让我们通过一个简单的例子,使用此类走路:

class Foo<T> 
{ 
    public T SomeProperty { get; set; } 
}

在C#编译器编译这个例子中,它会在typedef的元数据表定义富,像它会对于任何其他类型。与非泛型类型不同,GenericParam表中还会有一个条目描述其泛型参数(index = 0,flags =?,name =(指向String heap,“T”的索引),owner = type“Foo “)。

TypeDef表中的其中一列数据是MethodDef表的起始索引,该索引是在此类型上定义的方法的连续列表。对于Foo,我们定义了三个方法:一个getter和一个setter到SomeProperty和一个由编译器提供的默认构造函数。因此,MethodDef表将为每个这些方法保留一行。 MethodDef表中的重要栏之一是“签名”栏。此列存储对描述方法确切签名的字节blob的引用。 ECMA-335详细介绍了这些元数据签名blob,因此我不会在这里反复出现这些信息。

方法签名blob包含有关参数的类型信息以及返回值。在我们的例子中,setter需要一个T,而getter返回一个T.那么,什么是T呢?在签名blob中,它将是一个特殊值,意思是“索引0处的泛型类型参数”。这意味着GenericParams表中索引= 0的行与owner = type“Foo”,这是我们的“T”。

自动属性后备存储字段也是一样。 TypeDef表中的Foo条目将在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 = <对.ctor >的预期签名blob的引用。

TypeSpecToken1将引用TypeSpec表中的第一行。从上面我们知道这个表存储了泛型类型的实例。在这种情况下,该行将包含对“Foo <int>”的签名blob的引用。因此,MemberRefToken1确实表示我们正在引用“Foo <int> .ctor()”。

MemberRefToken1MemberRefToken2将共享相同的等级值,即​​TypeSpecToken1。但是,他们会在名称和签名blob上有所不同(MethodRefToken2将用于“set_SomeProperty”)。同样,MemberRefToken3MemberRefToken4将分享TypeSpecToken2,“富<串>”的实例,但以同样的方式将名字和BLOB不同。

当JIT编译器编译上述CIL,它注意到它看到一个通用实例化它(即富<INT>或富<串>)之前还没有见过。接下来发生的事情由Shiv Kumar的答案覆盖得很好,所以我在这里不再重复。简而言之,当JIT编译器遇到新的实例化泛型类型时,它可能会使用实例中的实际类型替换泛型参数,在其类型系统中使用字段布局发布整个新类型。他们也会有自己的方法表,每种方法的JIT编译都会涉及用实例化的实际类型替换对泛型参数的引用。JIT编译器也有责任强制执行CIL的正确性和可验证性。所以总结一下:C#编译器发出元数据来描述什么是泛型的,泛型类型/方法是如何实例化的。 JIT编译器使用此信息在运行时为实例化的泛型类型发出新类型(假定它与现有实例不兼容),并且每种类型都将拥有自己的已根据实际使用的类型编译的代码副本在实例化中。

希望这是有道理的一些小方法。

+0

我试图把我的牙齿埋入这个答案。我将尝试绘制你刚刚放在这里的东西,这样我就可以看到它。 – richard 2011-02-24 06:44:20

+0

我很抱歉,这是很多消化。简洁地回答这是一个棘手的问题:)。 – 2011-02-24 07:00:08

+0

此外,如果您好奇,可以使用ildasm.exe以友好的方式查看元数据表,方法是转到查看 - >元数据 - >显示! (这不在我头顶,可能不完全准确)。拆卸方法时,您还可以“显示令牌”和“显示字节”。在那里你会看到带有元数据标记(如newobj)的指令的4字节标记操作数。 ECMA-335具有完整的指令参考,并详细介绍了每条指令的评估栈转换;当我开始学习CLR的内部工作时,我发现它非常宝贵。 – 2011-02-24 07:22:11

对于值类型,在运行时为每个值类型泛型类定义了一个特定的“类”。对于引用类型,只有一个类定义可以在不同类型中重用。

我在这里简化,但这就是概念。泛型为 .NET公共语言运行时

我们的方案

设计与实现大致运行过程如下: 当运行时需要特定 实例化一个参数 类,装载机检查是否实例化与它之前见过的任何 兼容;如果不是,则 确定一个现场布局,并创建新的 vtable,在所有兼容实例之间共享 。 此vtable中的项目是条目 存根的类的方法。 当这些存根被稍后调用时,它们将生成(“及时”) 代码以便为所有兼容的 实例化共享。当在一个特定的 实例编译 调用(非虚) 多态的方法,我们首先检查,看看

如果我们 一些兼容的实例之前编译这样的呼吁;如果不是 ,则生成条目存根 ,其将生成码为 共享用于所有兼容的 实例化。两个实例是 兼容如果对于任何参数 类其在这些 实例汇编引起相同 代码等执行结构 (例如音响场布局和GC表),从字典 开的描述下面 在第4.4节。特别是, 所有的参考类型相互兼容 ,因为装载程序 和JIT编译器不区分 用于现场布局或 代码生成。在Intel x86的实现 上,如果它们具有相同的大小(至少具有不同的参数传递 约定),则至少基本类型是互不兼容的,即使是 。这留下了用户定义的 结构类型,如果其 的布局与 相同,则它们是兼容的,以便它们共享 跟踪的指针的相同模式。

http://research.microsoft.com/pubs/64031/designandimplementationofgenerics.pdf

+1

这是一篇非常棒的论文。感谢您的链接。 – richard 2011-02-24 05:10:37