C#/ .Netgenerics如何知道它们的参数类型?

在C#中,generics函数或类知道其generics参数的类型。 这意味着动态类型信息,如是或可用(与Java不同)。

我很好奇,编译器如何将这种类型信息提供给generics方法? 对于类我可以想象实例可以简单地有一个指向类型的指针,但对于generics函数我不确定,也许只是一个隐藏的参数?

如果generics被保存到IL级别,我相信它们是,那么我想知道在这个级别如何完成。

由于您已经编辑了问题以将其扩展到C#编译器以外的JIT编译器,因此这里以List为例说明了该过程。

正如我们已经建立的那样, List类只有一个IL表示。 该表示具有与C#代码中看到的T类型参数对应的类型参数。 正如Holger Thiemann在他的评论中所说,当您使用具有给定类型参数的List<>类时,JIT编译器为该类型参数创建类的本机代码表示。

但是,对于引用类型,它仅编译本机代码一次,并将其重用于所有其他引用类型。 这是可能的,因为在虚拟执行系统(VES,通常称为“运行时”)中, 只有一种引用类型,在规范中称为O (参见标准中的第I.12.1节,表I.6: http : http ://www.ecma-international.org/publications/standards/Ecma-335.htm )。 此类型定义为“对托管内存的本机大小对象引用”。

换句话说,VES的(虚拟)评估堆栈中的所有对象由“对象引用”(实际上是指针)表示,该对象引用本身基本上是无类型的。 那么VES如何确保我们不使用不兼容类型的成员? 什么阻止我们调用System.Random实例上的string.Length属性?

为了强制实施类型安全,VES使用描述每个对象引用的静态类型的元数据,将方法调用接收器的类型与方法的元数据令牌标识的类型进行比较(这也适用于其他成员类型的访问)。

例如,要调用对象类的方法,对对象的引用必须位于虚拟评估堆栈的顶部。 由于方法的元数据和“堆栈转换”的分析 – 每个IL指令引起的堆栈状态的变化,因此该引用的静态类型是已知的。 然后, callcallvirt指令通过包括表示方法的元数据标记来指示要调用的方法,该元数据标记当然指示定义方法的类型。

VES在编译之前“validation”代码,将引用的类型与方法的类型进行比较。 如果类型不兼容,则validation失败,程序崩溃。

这对于generics类型参数也适用于非generics类型。 为此,VES限制了可以在类型为无约束generics类型参数的引用上调用的方法。 唯一允许的方法是在System.Object定义的方法,因为所有对象都是该类型的实例。

对于约束参数类型,该类型的引用可以接收对约束类型定义的方法的调用。 例如,如果编写一个方法,其中约束类型T要从ICollection派生,则可以在类型为T的引用上调用ICollection.Count getter。 VES知道调用此getter是安全的,因为它确保存储到堆栈中该位置的任何引用都将是实现ICollection接口的某种类型的实例。 无论对象的实际类型是什么,JIT编译器都可以使用相同的本机代码。

还要考虑依赖于generics类型参数的字段。 在List的情况下,有一个T[]类型的数组,用于保存列表中的元素。 请记住,实际的内存数组将是一个O对象引用数组。 无论数组是List还是List的成员,构造该数组或读取或写入其元素的本机代码看起来都是一样的。

因此,在List类的无约束generics类型的范围内, T引用与System.Object引用一样好。 但是,generics的优点是VES将类型参数替换为调用者范围中的type参数。 换句话说,即使ListList内部处理它们的元素相同,调用者也会看到一个的Find方法返回一个string ,而另一个的Find方法返回一个FileInfo

最后,因为所有这些都是通过IL中的元数据实现的,并且因为VES在加载时使用元数据并且JIT编译类型,所以可以在运行时通过reflection提取信息。

您询问了强制转换(包括isas )如何处理generics类型参数的变量。 由于所有对象都存储有关其自身类型的元数据,因此所有强制转换的工作方式与使用变量类型object 。 该对象被询问其类型并且正在进行运行时决策。

当然,这种技术仅对参考类型有效。 对于值类型,JIT为每个用于实例化generics类型参数的值类型编译一个专用的本机方法。 在那种专门的方法中, T的类型是完全已知的。 不需要进一步的“魔力”。 因此,值类型参数是“无聊”的情况。 对于JIT,看起来根本没有generics类型参数。

typeof(T)如何工作? 此值作为隐藏参数传递给generics方法。 这也是someObj as T能够工作的方式。 我很确定它被编译为对运行时助手的调用(例如RuntimeCastHelper(someObj, typeof(T)) )。

clr运行时只是在第一次执行时分别编译每个方法。 您可以看到这一点,如果您在具有多行的方法中使用某个类型,并且缺少定义类型的DLL。 在方法的第一行中设置断点。 在调用该方法时,抛出一个类型加载exception。 调试器不会触发断点。 现在将方法分为三个子方法。 中间的一行应该包含缺少类型的行。 现在您可以使用调试器和第一个新方法进入方法,但是在调用第二个方法时,抛出exception。 这是因为该方法在首次调用时被编译,并且只有编译器/链接器在丢失的类型上发生故障。

回答你的问题:正如其他人所指出的那样,IL中支持generics。 在执行时,当您第一次创建List时,将编译构造函数代码(使用int参数替换类型参数)。 如果您是第一次创建List,则会再次使用string作为类型参数编译代码。 您可以看到它就像在运行时动态生成具体类型的具体类一样。

how does the compiler provides this type information to the generic methods?

tl; dr 它通过有效地复制与其一起使用的每个唯一类型的方法来提供类型信息。

现在,对于那些想要阅读更多内容的人来说……;)一旦你得到一个小例子,答案实际上很简单。

让我们从这开始:

 public static class NonGenericStaticClass { public static string GenericMethod(T value) { if(value is Foo) { return "Foo"; } else if(value is Bar) { return "Bar"; } else { return string.Format("It's a {0}!", typeof(T).Name); } } } // ... static void Main() { // Prints "It's a Int32!" Console.WriteLine(NonGenericStaticClass.GenericMethod(100)); // Prints "Foo" Console.WriteLine(NonGenericStaticClass.GenericMethod(new Foo())) // Prints "It's a Int32!" Console.WriteLine(NonGenericStaticClass.GenericMethod(20)); } 

现在,正如其他人已经说过的那样,IL本身支持generics,所以C#编译器实际上并没有对这个例子做太多贡献。 但是,当Just-In-Time编译器出现将IL转换为机器代码时,它必须将通用代码转换为非通用代码。 为此,.Net Just-In-Time编译器有效地复制了与其一起使用的每种不同类型的方法。

如果生成的代码在C#中,它可能看起来像这样:

 public static class NonGenericStaticClass { // The JIT Compiler might rename these methods after their // representative types to avoid any weird overload issues, but I'm not sure public static string GenericMethod(Int32 value) { // Note that the JIT Compiler might optimize much of this away // since the first 2 "if" statements are always going to be false if(value is Foo) { return "Foo"; } else if(value is Bar) { return "Bar"; } else { return string.Format("It's a {0}!", typeof(Int32).Name); } } public static string GenericMethod(Foo value) { if(value is Foo) { return "Foo"; } else if(value is Bar) { return "Bar"; } else { return string.Format("It's a {0}!", typeof(Foo).Name); } } } // ... static void Main() { // Notice how we don't need to specify the type parameters any more. // (of course you could've used generic inference, but that's beside the point), // That is because they are essentially, but not necessarily, overloads of each other // Prints "It's a Int32!" Console.WriteLine(NonGenericStaticClass.GenericMethod(100)); // Prints "Foo" Console.WriteLine(NonGenericStaticClass.GenericMethod(new Foo())) // Prints "It's a Int32!" Console.WriteLine(NonGenericStaticClass.GenericMethod(20)); } 

一旦你生成了非generics方法,那么你就可以通过静态调度的精彩使用确切地知道你正在处理什么类型。

现在,我代表转型的方式与实际完成方式之间显然存在差异,但这就是它的要点。 此外,对于generics类型也执行相同类型的过程。

对于某些对比 ,Java编译器“欺骗”generics。 Java不会生成类似.Net的新类型和方法,而是插入您期望值为某种类型的转换。 因此,我们的typeof(T)在Java世界中是不可能的,相反我们必须使用getClass()方法。