【发布时间】:2014-01-29 19:41:23
【问题描述】:
在为我设计的玩具语言开发编译器时,我环顾了一下在语言中实现泛型有哪些选择(通过搜索现有语言的示例),我开始想知道 C# 泛型。
我将首先尝试描述我所理解的内容。如果我有任何误解,请随时纠正我。我将使用术语 generic class/type/template/definition 来指代 List<T> 和 concrete class/type 来指代 List<int> 之类的东西, List<string>等
关于帖子的长度提前道歉。
C++ 使用模板进行泛型编程。据我了解,这意味着:
- C++ 编译器在遇到模板定义时,只需 将其文本(或文本的等价物)保存在内存中。
- 然后,每次遇到对(新)具体类型的引用时 在代码中,参考了通用模板,文本为 生成请求的具体类型(通过替换类型 具有请求组合的参数),最后该代码是 编译并添加到二进制文件中。
模板本身在二进制文件中不可用,因为它只是一种文本表示,而不是具体类型。只有从它生成的具体类型是可见的。
(我我不确定最后一部分 - 正如我之前所说,如果我离开了,请纠正我。)
Java 使用类型擦除,这意味着仅在编译时检查所有泛型类型参数的类型安全性,然后(如果未检测到类型不匹配),它们将全部替换为对 @ 的引用987654328@ 基类型,有效地为所有具体类型引用重用一个(非泛型)类。
现在,进入实际问题。
在阅读an interview of Anders Hejlsberg(不是具体链接的那个,但他的观点是一样的)之后,他批评了Java的类型擦除,我认为C#不使用类型擦除。而且,由于我们可以反思 C# 中的具体类型,并且我们确实有 LINQ 之类的东西(这涉及到 C# 的泛型功能的相当复杂的用法),我们可以肯定地说 C# 确实不像 Java 一样使用类型擦除。
不知道任何其他选项,我假设 C# 使用 类似 模板。 不完全是 C++ 模板(至少我理解它们),因为我们显然可以创建一个具体类型,其通用版本在另一个程序集中进行了描述,并且因为我们可以再次反映该类型并获取信息关于它。所以,我认为 C# 泛型更像是模板加元数据。
在某些时候,我在某处(虽然我找不到链接)读到 C# 泛型类实际上是在幕后的抽象类。显然,这与我认为我知道的不一致——很奇怪。
几个月前我都忘记了它,但今天我偶然发现了a question on SO very similar to this one(不幸的是,它没有明确的答案)。该问题的作者证明 C# 编译器不会在编译时对泛型进行方法解析,即使对于在编译时可以已知的类型(显示使用他创建的 new 方法来隐藏object.GetHashCode)。
好的,所以 C# 泛型肯定不是“文本替换加元数据”,正如我最初所想的那样。如果是这样,那么在那个问题中,Test 的(文本生成的)具体类型将导致编译器以非常不同的方式解析 GetHashCode 调用。
但是,相反,C# 编译器将其解析为好像类型只不过是object,对于泛型类型的所有具体实现,包括 new GetHashCode 如果它是非通用代码,则将被解析。这使得 C# 泛型看起来更接近于类型擦除加元数据。现在我知道这个术语不是很贴切:如果每个具体类的元数据都维护类型参数信息,它就不是真正的类型擦除——但它确实类似于 Java 将所有内容存储为对象的方法(至少对于引用类型,它本质上是可在低级别互换)和来回施法。
我试图想象第三种可能性(正如我所说,我找不到任何来源 - 所以请谨慎对待),泛型类在幕后被表示为抽象类,每次生成新的具体类型时,编译器都会对其进行扩展和智能专门化,但我无法完全理解它在实践中是如何工作的。例如,泛型类的修饰符sealed 的语义必须“转移”以允许扩展that 类(在程序集中),但不允许扩展其子类。一般来说,我认为它会使编译器(可能还有运行时)变得非常复杂,我什至无法理解。
那么,泛型是如何在 C# 中真正实现的?虽然它们肯定存在差异,但 它们更接近 C++ 还是更接近 Java?泛型定义真的表示为程序集中的一个特殊抽象类吗?或者它可能与我所描述的完全不同?
我不是在寻找一个特别详细的答案(尽管它会受到欢迎)。简单的解释就可以了,只要它清楚地突出 C#/Java/C++ 之间的差异,并至少为我提供 C# 编译器和运行时如何处理泛型类的理论知识。
编辑#1:我知道 Eric Lippert 强调了 C++ 模板和 C# 泛型 in at least one blog post 之间的差异,本质上是说“C# 泛型不是模板”,但我不知道它们是什么的任何解释 在幕后。
编辑#2:the linked question 的答案没有解决我要问的问题。该答案简要解释了作为底层实现结果的特定示例或现象,但它绝对没有解释所要求的内容:底层实现是什么。
【问题讨论】:
-
在 C++ 情况下更准确地说:它是从模板生成代码的预处理器,然后被编译。你说得对,编译结果中没有模板的痕迹(嗯,间接 - 在生成的类的名称中)。
-
这是 Eric Lippert 等人的任意数量博客文章的副本。人。
-
我认为这更像是一个 IL 问题。也许您正在寻找的答案是 JIT-er 如何编译通用 IL 方法。对于我所看到的 C# 编译器,普通类型和泛型类型之间没有太大区别。泛型支持已融入 CLR,我认为您不能仅将其视为 C# 编译器功能。
-
底层实现是:C#编译器生成IL。 JIT 编译器从 IL 生成机器代码。当抖动第一次遇到使用一组特定类型参数实例化的泛型方法或泛型类型方法时,它会为该实例化生成新的机器代码。抖动足够聪明,可以为所有引用类型的类型参数重用现有的机器代码。也就是说,如果
List<string>.Add已被jit,那么对List<object>.Add的调用将调用List<string>.Add的代码。你明白为什么这是合法的吗? -
但是
List<int>.Add和List<double>.Add是各自独立的;值类型的类型参数触发新的代码生成。这与 C++(所有编译都在编译时完成)和 Java(其中List<int>被视为List<object>—— int 被装箱)形成对比。
标签: c# templates generics compiler-construction