如何实现虚拟泛型方法调用
本文关键字:虚拟 泛型方法 调用 实现 何实现 | 更新日期: 2023-09-27 18:01:12
我对CLR如何实现这样的调用很感兴趣:
abstract class A {
public abstract void Foo<T, U, V>();
}
A a = ...
a.Foo<int, string, decimal>(); // <=== ?
这个调用是否会导致某种哈希映射查找,将类型参数标记作为键,将编译的泛型方法专用化(一个用于所有引用类型,另一个用于不同的代码用于所有值类型)作为值?
我没有找到太多关于这方面的确切信息,所以这个答案大部分是基于2001年(甚至在.Net 1.0问世之前!)关于.Net泛型的优秀论文,后续论文中的一个简短注释,以及我从SSCLI v.2.0源代码中收集的内容(尽管我找不到调用虚拟泛型方法的确切代码)。
让我们简单地开始:如何调用非泛型非虚拟方法?通过直接调用方法代码,使编译后的代码包含直接地址。编译器从方法表中获取方法地址(见下一段)。能这么简单吗?嗯,差不多了。方法是JITed的事实使它变得有点复杂:实际调用的是编译方法的代码,如果还没有编译,则只执行它;或者它是一条直接调用已编译代码的指令,如果它已经存在的话。我将进一步忽略这个细节。
现在,如何调用非泛型虚拟方法?与C++等语言中的多态性类似,有一个方法表可从this
指针(引用)访问。每个派生类都有自己的方法表及其方法。因此,要调用一个虚拟方法,请获取对this
的引用(作为参数传入),然后获取对方法表的引用,查看其中正确的条目(对于特定函数,条目号是恒定的),然后调用条目指向的代码。通过接口调用方法稍微复杂一些,但现在对我们来说并不感兴趣。
现在我们需要了解代码共享。如果类型参数中的引用类型对应于任何其他引用类型,并且值类型完全相同,则代码可以在同一方法的两个"实例"之间共享。因此,例如C<string>.M<int>()
与C<object>.M<int>()
共享代码,但不与C<string>.M<byte>()
共享代码。类型类型参数和方法类型参数之间没有区别。(2001年的原始论文提到,当两个参数都是具有相同布局的struct
时,代码也可以共享,但我不确定在实际实现中这是真的。)
让我们在通往泛型方法的道路上迈出中间一步:泛型类型中的非泛型方法。由于代码共享,我们需要从某处获取类型参数(例如,用于调用类似new T[]
的代码)。因此,泛型类型(例如C<string>
和C<object>
)的每个实例化都有自己的类型句柄,其中包含类型参数和方法表。普通方法可以从this
引用访问此类型句柄(从技术上讲,这是一个被混淆地称为MethodTable
的结构,尽管它包含的不仅仅是方法表)。有两种类型的方法不能做到这一点:静态方法和值类型上的方法。对于这些类型,类型句柄作为隐藏参数传入。
对于非虚拟泛型方法,类型句柄是不够的,因此它们会得到不同的隐藏参数MethodDesc
,该参数包含类型参数。此外,编译器不能将实例化存储在普通方法表中,因为这是静态的。因此,它为泛型方法创建了第二个不同的方法表,该表由类型参数索引,如果已经存在兼容的类型参数,则从中获取方法地址,或者创建一个新条目。
虚拟泛型方法现在很简单:编译器不知道具体的类型,所以它必须在运行时使用方法表。普通方法表不能使用,所以它必须在特殊方法表中查找通用方法。当然,包含类型参数的隐藏参数仍然存在。
在研究这一点时学到了一个有趣的花絮:因为JITer非常懒惰,所以以下(完全无用的)代码可以工作:
object Lift<T>(int count) where T : new()
{
if (count == 0)
return new T();
return Lift<List<T>>(count - 1);
}
等效的C++代码导致编译器放弃堆栈溢出。
是。特定类型的代码由CLR在运行时生成,并保留实现的哈希表(或类似的)。
CLR第372页,通过C#:
当使用泛型类型的方法参数是JIT编译的,CLR取方法的IL,替换指定的类型参数,然后创建特定的本机代码在指定的数据类型。这正是你想要什么,是主要的泛型的特征。然而是一个缺点:CLR保持为每个生成本机代码方法/类型组合。这是称为代码爆炸。这最终可能会增加应用程序的工作集实质上,从而伤害表演幸运的是,CLR有一些内置优化以减少代码爆炸。首先,如果一个方法调用特定类型的自变量,然后,再次调用该方法使用相同的类型参数,CLR将为此编译代码方法/类型组合仅一次所以如果一个程序集使用List,和一个完全不同的组件(加载在同一AppDomain中)使用List,CLR将编译List的方法只有一次。这减少了代码爆炸非常
编辑
我现在遇到了https://msdn.microsoft.com/en-us/library/sbh15dya.aspx它清楚地指出,泛型在使用引用类型时重用相同的代码,因此我接受这一点作为权威。
原始答案
我在这里看到了两个不一致的答案,而且都提到了他们的观点,所以我会努力加上我的两分钱。
首先,微软出版社出版的杰弗里·里希特的《Clr via C#》与msdn博客一样有效,尤其是因为该博客已经过时了http://www.amazon.com/Jeffrey-Richter/e/B000APH134人们必须承认他是windows和.net方面的专家)
现在让我做我自己的分析。
显然,包含不同引用类型参数的两个泛型类型不能共享相同的代码
例如,List<类型A>并且List<类型B>>不能共享相同的代码,因为这将导致将TypeA对象添加到List<类型B>通过反射,clr也是基于遗传学的强类型(不像Java,在Java中只有编译器验证泛型,但底层JVM对此一无所知)。
这不仅适用于类型,也适用于方法,因为例如,T类型的泛型方法可以创建T类型的对象(例如,没有什么可以阻止它创建新的List<T>),在这种情况下,重复使用相同的代码会造成严重破坏。
此外,GetType方法是不可重写的,事实上它总是返回正确的泛型类型,从而确保每个类型参数都有自己的代码。(这一点甚至比看起来更重要,因为clr和jit是基于为该对象创建的类型对象工作的,使用GetType(),这只是意味着对于每个类型参数,即使对于引用类型,也必须有一个单独的对象)
代码重用可能导致的另一个问题是,as和as运算符将不再正确工作,通常所有类型的强制转换都会出现严重问题。
现在进行实际测试:
我测试了它,使用了一个包含静态成员的泛型类型,然后创建了两个具有不同类型参数的对象,静态字段完全不共享,这清楚地表明,即使对于引用类型,代码也不共享。
编辑:
请参阅http://blogs.msdn.com/b/csharpfaq/archive/2004/03/12/how-do-c-generics-compare-to-c-templates.aspx关于如何实现:
空间使用
C++和C#对空间的使用不同。因为C++模板是在编译时完成的,每次都在模板导致由编译器。
在C#世界中,情况有所不同。实际实现使用特定类型在运行时创建。当运行时创建一个类似List的类型,JIT将查看它是否已经创建。如果有,它只会使用该代码。如果没有,那就需要编译器生成并进行适当替换的IL具有实际类型。
这不太正确。有一个单独的本机代码路径每个值类型,但由于引用类型都是引用大小的,他们可以分享他们的实现。
这意味着C#方法应该在磁盘和内存,所以这是泛型相对于C的优势++模板。
事实上,C++链接器实现了一个称为"模板"的特性折叠",链接器在其中查找完全相同,如果它找到它们,就把它们折叠在一起。所以这不是
正如我们所看到的,CLR"可以"为引用类型重用实现,就像当前的c++编译器一样,但这并不能保证,对于使用stackalloc和指针的不安全代码,情况可能并非如此,也可能存在其他情况。
然而,我们必须知道的是,在CLR类型系统中,它们被视为不同的类型,例如对静态构造函数的不同调用、单独的静态字段、单独的类型对象,并且类型自变量T1的对象不应该能够访问具有类型自变量T2的另一对象的私有字段(尽管对于相同类型的对象,确实可以访问来自相同类型的另一个对象的私有域)。