在C#中,泛型函数或类知道其泛型参数的类型。这意味着可以使用动态类型信息,例如is
或as
(与Java不同)。
我很好奇,编译器如何将这种类型信息提供给泛型方法?对于我可以成像的类,实例可以只是指向类型的指针,但对于通用函数,我不确定,可能只是一个隐藏的参数?
如果泛型保留在IL级别,我相信它们是,那么我想知道在该级别如何完成。
答案 0 :(得分:5)
由于您已经编辑了问题以将其扩展到C#编译器以外的JIT编译器,因此以List<T>
为例说明了该过程的概述。
正如我们已经建立的那样,List<T>
类只有一个IL表示。此表示形式具有与C#代码中看到的T
类型参数对应的类型参数。正如Holger Thiemann在他的评论中所说,当你使用具有给定类型参数的List<>
类时,JIT编译器为该类型参数创建类的本机代码表示。
但是,对于引用类型,它只编译本机代码一次,并将其重用于所有其他引用类型。这是可能的,因为在虚拟执行系统(VES,通常称为“运行时”)中,在规范中只有一个名为O
的引用类型(参见第I.12.1节) ,表I.6,标准:http://www.ecma-international.org/publications/standards/Ecma-335.htm)。此类型被定义为“对托管内存的本机大小对象引用。”
换句话说,VES的(虚拟)评估堆栈中的所有对象都由“对象引用”(实际上是指针)表示,该对象引用本身基本上是无类型的。那么VES如何确保我们不使用不兼容类型的成员?是什么阻止我们在string.Length
的实例上调用System.Random
属性?
为了强制实施类型安全,VES使用描述每个对象引用的静态类型的元数据,将方法调用接收器的类型与方法的元数据标记所标识的类型进行比较(这也适用于其他成员类型的访问) )。
例如,要调用对象类的方法,对对象的引用必须位于虚拟评估堆栈的顶部。由于方法的元数据和“堆栈转换”的分析 - 每个IL指令引起的堆栈状态的变化,因此该引用的静态类型是已知的。然后,call
或callvirt
指令通过包含表示方法的元数据标记来指示要调用的方法,该元数据标记当然指示定义方法的类型。
VES在编译之前“验证”代码,将引用的类型与方法的类型进行比较。如果类型不兼容,则验证失败,程序崩溃。
这对于泛型类型参数也适用于非泛型类型。为了实现这一点,VES限制了可以在类型为无约束泛型的引用上调用的方法类型参数。唯一允许的方法是在System.Object
上定义的方法,因为所有对象都是该类型的实例。
对于约束参数类型,该类型的引用可以接收对约束类型定义的方法的调用。例如,如果您编写的方法中约束类型T
要从ICollection
派生,则可以在ICollection.Count
类型的引用上调用T
getter。 VES知道调用此getter是安全的,因为它确保存储到堆栈中该位置的任何引用都是实现ICollection
接口的某种类型的实例。无论对象的实际类型是什么,JIT编译器都可以使用相同的本机代码。
还要考虑依赖于泛型类型参数的字段。在List<T>
的情况下,有一个类型为T[]
的数组,用于保存列表中的元素。请记住,实际的内存数组将是O
对象引用的数组。无论数组是List<string>
还是List<FileInfo>
的成员,构造该数组或读取或写入其元素的本机代码看起来都是一样的。
因此,在List<T>
之类的无约束泛型类型的范围内,T
引用与System.Object
引用一样好。但是,泛型的优点是VES将类型参数替换为调用者范围中的type参数。换句话说,即使List<string>
和List<FileInfo>
将其元素视为内部相同的 ,调用者也会看到一个Find
方法返回{{1}而另一个的返回string
。
最后,因为所有这些都是通过IL中的元数据实现的,并且因为VES在加载时使用元数据并且JIT编译类型,所以可以在运行时通过反射提取信息。
答案 1 :(得分:1)
您询问了强制转换(包括is
和as
)如何处理泛型类型参数的变量。由于所有对象都存储有关其自身类型的元数据,因此所有强制转换的工作方式与使用变量类型object
的方式相同。该对象被询问其类型,并且正在做出运行时决定。
当然这种技术仅适用于参考类型。对于值类型,JIT为每个用于实例化泛型类型参数的值类型编译一个专用的本机方法。在该专门方法中,T
的类型是完全已知的。不需要进一步的“魔力”。因此,值类型参数是“无聊”的情况。对于JIT,看起来根本没有泛型类型参数。
typeof(T)
如何运作?此值作为隐藏参数传递给泛型方法。这也是someObj as T
能够工作的方式。我很确定它被编译为对运行时助手的调用(例如RuntimeCastHelper(someObj, typeof(T))
)。
答案 2 :(得分:0)
clr运行时只是在第一次执行时分别编译每个方法。您可以看到这一点,如果您在具有多行的方法中使用某个类型,并且缺少定义类型的DLL。在方法的第一行中设置断点。在调用该方法时,抛出一个类型加载异常。调试器不会触发断点。现在将方法分为三个子方法。中间的一行应该包含缺少类型的行。现在,您可以使用调试器进入方法,也可以进入第一个新方法,但是在调用第二个方法时,抛出异常。这是因为该方法在第一次调用时被编译,只有编译器/链接器在丢失的类型上发生故障。
回答你的问题:正如其他人所指出的那样,IL中支持泛型。在执行时,当您第一次创建List时,将编译构造函数代码(使用int参数替换类型参数)。如果您第一次创建List,则会再次使用string作为类型参数编译代码。您可以看到它好像具体类型的具体类是在运行时生成的。
答案 3 :(得分:0)
how does the compiler provides this type information to the generic methods?
<强> TL;博士强> 它通过有效地复制与其一起使用的每种唯一类型的方法来提供类型信息。
现在,对于那些想要阅读更多内容的人...;) 一旦你得到一个小例子,答案实际上很简单。
让我们从这开头:
public static class NonGenericStaticClass
{
public static string GenericMethod<T>(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<int>(100));
// Prints "Foo"
Console.WriteLine(NonGenericStaticClass.GenericMethod<Foo>(new Foo()))
// Prints "It's a Int32!"
Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(20));
}
现在,正如其他人已经说过的那样,IL本身支持泛型,所以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));
}
一旦您生成了非泛型方法,那么您就可以通过static dispatch的精彩使用确切地知道您正在处理的类型。
现在,我代表转换的方式与实际完成方式之间显然存在差异,但这就是它的要点。 此外,对于泛型类型也进行了相同类型的处理。
For some contrast,Java编译器&#34;作弊&#34;仿制药。 Java不会生成像.Net那样的新类型和方法,而是在您期望值为特定类型的位置插入转换。
因此,我们的typeof(T)
在Java世界中是不可能的,而是我们必须使用getClass()
方法。