c# - NET公共(public)语言运行时的泛型实现是什么

标签 c# .net generics cil

当您在C(或一般的.NET)中使用泛型集合时,编译器是否基本上完成了开发人员过去为特定类型生成泛型集合所必须做的腿部工作。所以基本上。…它只是帮我们节省了工作?
现在我想,这不可能是正确的。因为没有泛型,所以我们不得不在内部使用非通用数组来进行集合,因此存在装箱和拆箱(如果是值类型的集合)等。
那么,泛型在cil中是如何呈现的呢?当我们说我们想要一个通用的东西集合时,它在做什么呢?我不一定需要cil代码示例(尽管这没关系),我想知道编译器如何获取泛型集合并呈现它们的概念。
谢谢!
我知道我可以用ildasm来看待这个问题,但在我看来,我仍然是中国人,我还没有准备好解决这个问题。我只想知道c(以及其他我猜也是的语言)如何在cil中呈现的概念来处理泛型。

最佳答案

请原谅我冗长的帖子,但这个话题相当宽泛。我将尝试描述C编译器发出什么,以及JIT编译器在运行时如何解释它。
ECMA-335(这是一个写得非常好的设计文档;请查看)是了解.net程序集中如何表示所有内容(我是说所有内容)的最佳位置。程序集中有几个相关的cli元数据表可用于获取常规信息:
genericparam-存储有关泛型参数(索引、标志、名称、所属类型/方法)的信息。
genericparamconstraint-存储有关泛型参数约束(拥有泛型参数、约束类型)的信息。
methodspec-存储实例化的泛型方法签名(例如bar.method表示bar.method)。
typespec-存储实例化的泛型类型签名(例如bar表示bar)。
因此,考虑到这一点,让我们通过一个简单的例子来使用这个类:

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

当c编译器编译这个示例时,它将在typedef元数据表中定义foo,就像对任何其他类型一样。与非泛型类型不同,它还将在genericparam表中有一个描述其泛型参数(index=0,flags=?,name=(索引到字符串堆“t”),owner=类型“foo”)。
typedef表中的数据列之一是methoddef表的起始索引,methoddef表是在此类型上定义的方法的连续列表。对于foo,我们定义了三个方法:someproperty的getter和setter以及编译器提供的默认构造函数。因此,methoddef表将为每个方法保存一行。methoddef表中的一个重要列是“signature”列。此列存储对一个字节块的引用,该字节块描述方法的确切签名。ECMA-335详细介绍了这些元数据签名blob,因此我不会在这里反刍这些信息。
方法签名blob包含有关参数和返回值的类型信息。在我们的例子中,setter取t,getter返回t。那么,t是什么?在签名blob中,它将是一个特殊值,表示“索引0处的泛型类型参数”。这意味着genericparams表中的行的index=0,owner=type“foo”,这是我们的“t”。
auto property backing store字段也是如此。typedef表中的foo条目将有一个字段表的起始索引,字段表有一个“signature”列。字段的签名将表示字段的类型是“索引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是引用MemberRef元数据表中的行的元数据标记(标记是四个字节的值,其中最重要的字节是元数据表标识符,其余三个字节是行号,基于1)。此表存储对方法或字段的引用。在泛型之前,这是一个表,用于存储在引用程序集中定义的类型中使用的方法/字段的信息。但是,它也可以用于引用一般实例化中的成员。所以假设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)。接下来发生的事情被希夫·库马尔的回答很好地覆盖了,所以我不会在这里详细重复。简单地说,当jit编译器遇到一个新的实例化泛型类型时,它可能会使用实例化中的实际类型代替泛型参数,通过字段布局向其类型系统发出一个全新的类型。它们还将拥有自己的方法表,而每个方法的jit编译将涉及到用实例中的实际类型替换对泛型参数的引用。jit编译器还负责强制cil的正确性和可验证性。
所以总结一下:c编译器发出元数据,描述什么是泛型以及泛型类型/方法是如何实例化的。jit编译器使用此信息在运行时为实例化的泛型类型发出新类型(假设它与现有的实例化不兼容),并且每个类型都有自己的代码副本,这些代码是基于实例化中使用的实际类型进行jit编译的。
希望这在某种程度上是有意义的。

关于c# - NET公共(public)语言运行时的泛型实现是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5099977/

相关文章:

时间:2019-03-17 标签:c#socketsusessl

c# - 从 Excel 电子表格读取数据 - 无法对空引用执行运行时绑定(bind)

c# - 如何在查询 LINQ 中表达 IN(参数列表)?

c# - C# 编译器能否插入在编译时每次使用时自动递增的常量值?

.net - Watin CaptureWebPageToFile() 生成黑色图像

c# - 为什么 volatile 和 MemoryBarrier 不阻止操作重新排序?

c# - 无法在 WPF C# 代码中获取对 png 作品的引用

c# - 将列表<B>转换到列表<A>

java - 泛型、方法签名、赋值

java - 具有泛型的 Hibernate 组件