ric*_*ard 13 .net c# generics cil
当你在C#(或一般的.NET)中使用泛型集合时,编译器基本上是否只需要为特定类型制作泛型集合的腿部工作开发人员.所以基本上...它只是节省了我们的工作?
现在我想到了,这不可能是正确的.因为没有泛型,我们以前必须在内部创建使用非泛型数组的集合,因此有装箱和拆箱(如果它是值类型的集合)等.
那么,如何在CIL中呈现泛型?当我们说我们想要一些通用的东西时,我们在做什么呢?我不一定想要CIL代码示例(虽然这样可以),我想知道编译器如何获取我们的泛型集合并呈现它们的概念.
谢谢!
PS我知道我可以用ildasm来看看这个,但我对CIL看起来仍然像中文,我还没准备好解决这个问题.我只想知道C#(以及我猜其他语言)如何在CIL中渲染以处理泛型的概念.
Pet*_*ene 14
请原谅我的详细帖子,但这个话题相当广泛.我将尝试描述C#编译器发出的内容以及JIT编译器在运行时如何解释它.
ECMA-335(它是一个写得非常好的设计文档;检查出来)是知道如何在.NET程序集中表示一切,我的意思是一切的地方.在程序集中有一些相关的CLI元数据表用于通用信息:
所以考虑到这一点,让我们来看一个使用这个类的简单示例:
class Foo<T>
{
public T SomeProperty { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
当C#编译器编译此示例时,它将在TypeDef元数据表中定义Foo,就像对任何其他类型一样.与非泛型类型不同,它还将在GenericParam表中有一个条目,用于描述其泛型参数(index = 0,flags = ?, name =(索引到String堆,"T"),owner = type"Foo" ").
TypeDef表中的一列数据是MethodDef表的起始索引,该表是在此类型上定义的连续方法列表.对于Foo,我们已经定义了三个方法:Someter的getter和setter以及编译器提供的默认构造函数.因此,MethodDef表将为每个方法保留一行.MethodDef表中的一个重要列是"签名"列.此列存储对blob字节的引用,该字节描述方法的确切签名.ECMA-335详细介绍了这些元数据签名blob,因此我不会在此处反复提供这些信息.
方法签名blob包含有关参数的类型信息以及返回值.在我们的例子中,setter取一个T,getter返回T.那么,什么是T呢?在签名blob中,它将是一个特殊值,表示"索引0处的泛型类型参数".这意味着GenericParams表中的行,其索引= 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";
Run Code Online (Sandbox Code Playgroud)
这将编译为类似这样的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")
Run Code Online (Sandbox Code Playgroud)
那么这个MemberRefToken业务是什么?MemberRefToken是一个元数据标记(标记是四个字节值,其中最重要的字节是元数据表标识符,其余三个字节是行号,从1开始),它引用MemberRef元数据表中的一行.此表存储对方法或字段的引用.在泛型之前,这是一个表,它将从引用的程序集中定义的类型中存储有关您正在使用的方法/字段的信息.但是,它也可以用于引用泛型实例化的成员.因此,假设MemberRefToken1引用MemberRef表中的第一行.它可能包含这样的数据:类= TypeSpecToken1,名字=".ctor",一滴= <参照.ctor的预期签名团块>.
TypeSpecToken1将引用TypeSpec表中的第一行.从上面我们知道这个表存储了泛型类型的实例.在这种情况下,此行将包含对"Foo <int>"的签名blob的引用.所以这个MemberRefToken1实际上是在说我们引用"Foo <int> .ctor()".
MemberRefToken1和MemberRefToken2将共享相同的类值,即TypeSpecToken1.但是,它们在名称和签名blob上会有所不同(MethodRefToken2将用于"set_SomeProperty").同样,MemberRefToken3和MemberRefToken4将共享TypeSpecToken2,即"Foo <string>"的实例化,但名称和blob以相同的方式不同.
当JIT编译器编译上面的CIL时,它注意到它看到了之前没有见过的泛型实例(即Foo <int>或Foo <string>).Shiv Kumar的回答很好地涵盖了接下来发生的事情,所以我在此不再赘述.简单地说,当JIT编译器遇到一个新的实例化的泛型类型,它可以代替通用参数的使用实际类型的实例发出了一个全新的类型转换成它的类型系统具有一个领域的布局.它们也有自己的方法表,每个方法的JIT编译都涉及用实例化中的实际类型替换对泛型参数的引用.JIT编译器还有责任强制执行CIL的正确性和可验证性.
总结一下:C#编译器发出描述通用内容以及如何实例化泛型类型/方法的元数据.JIT编译器在运行时使用此信息发出新类型(假设它与现有的实例化不兼容),用于实例化的泛型类型,并且每个类型都有自己的代码副本,这些代码已根据实际使用的类型进行了JIT编译在实例化中.
希望这在某种程度上是有道理的.
Shi*_*mar 10
对于值类型,在运行时为每个值类型泛型类定义了一个特定的"类".对于引用类型,只有一个类定义可以在不同类型中重用.
我在这里简化了,但这就是概念.
NET公共语言运行时泛型的设计与实现
我们的方案大致如下:当运行时需要参数化类的特定实例化时,加载器检查实例化是否与之前看到的任何实例化兼容; 如果不是,则确定现场布局并创建新的vtable,以在所有兼容的实例之间共享.此vtable中的项是类的方法的入口存根.当稍后调用这些存根时,它们将生成("即时")代码以便为所有兼容的实例共享.在特定实例化时编译(非虚拟)多态方法的调用时,我们首先检查一下
如果我们之前为一些兼容的实例化编译了这样的调用; 如果不是,则生成入口存根,其将依次生成要为所有兼容的实例共享的代码.两个实例是兼容的,如果对于任何参数化类,它在这些实例化的编译产生相同的代码和其他执行结构(例如现场布局和GC表),除了下面4.4节中描述的字典.特别是,所有引用类型都是相互兼容的,因为加载器和JIT编译器不区分现场布局或代码生成.至少在Intel x86的实现上,原始类型是互不兼容的,即使它们具有相同的大小(fl oats和int具有不同的参数传递约定).这留下了用户定义的结构类型,如果它们的布局与垃圾收集相同,则它们是兼容的,即它们共享相同的跟踪指针模式.
http://research.microsoft.com/pubs/64031/designandimplementationofgenerics.pdf