Copyright © 1996-2005 Artima Software, Inc. All rights reserved
Generics in C#, Java, and C++
A Conversation with Anders Hejlsberg, Part VII
by Bill Venners with Bruce Eckel. January 26, 2004
翻译:http://blog.csdn.net/lxwde
摘要
Anders Hejlsberg,C#的主架构师,与Bruce Eckel和Bill Venners 谈论了C#和Java的泛型、C++模板、C#的constraints特性以及弱类型化和强类型化的问题。
Anders Hejlsberg,微软的一位杰出工程师,他领导了C#(发音是C Sharp)编程语言的设计团队。Hejlsberg首次跃上软件业界舞台是源于他在80年代早期为MS-DOS和CP/M写的一个Pascal编译器。不久一个叫做Borland的非常年轻的公司雇佣了他并且买下了他的编译器,从那以后这个编译器就作为Turbo Pascal在市场上推广。在Borland,Hejlsberg继续开发Turbo Pacal并且在后来领导一个团队设计Turbo Pascal的替代品:Delphi。1996年,在Borland工作13年以后,Hejlsberg加入了微软,在那里一开始作为Visual J++和windows基础类库(WFC)的架构师。随后,Hejlsberg担任了C#的主要设计者和.NET框架创建过程中的一个主要参与者。现在,Anders Hejlsberg领导C#编程语言的后续开发。
2003年7月30号,Bruce Eckel(《Thinking in C++》以及《Thinking in Java》的作者)和Bill Venners(Artima.com的主编)与Anders Hejlsberg在他位于华盛顿州Redmond的微软办公室进行了一次面谈。这次访谈的内容将分多次发布在Artima.com以及Bruce Eckel将于今年秋天发布的一张音频光碟上。在这次访谈中,Anders Hejlsberg谈论了C#语言和.NET框架设计上的一些取舍。
· 在 第一部分:C#的设计过程中, Hejlsberg谈论了C#设计团队所采用的流程,以及在语言设计中可用性研究(usability studies)和好的品味(good taste)相对而言的优点。
· 在第二部分:Checked Exceptions的问题中, Hejlsberg谈论了已检测异常(checked exceptions)的版本(versionability)问题和规模扩展(scalability)问题。
· 在第三部分: 委托、组件以及表面上的简单性里,Hejlsberg 谈论了委托(delegates)以及C#对于组件的概念给予的头等待遇。
· 在第四部分:版本,虚函数和覆写里,Hejlsberg解释了谈论了为什么C#的方法默认是非虚函数,以及为什么程序员必须显式指定覆写(override)。
- 在第五部分:契约和互操作性里,Hejlsberg谈论了DLL hell、接口契约、strong anmes以及互操作的重要性。
- 在第七部分, Hejlsberg比较了C#和Java的泛型以及C++模板的实现方法,并且介绍了C#的constraints特性以及弱类型化和强类型化的问题。
泛型概述
Bruce Eckel: 能否就泛型做一个简短的介绍?
Anders Hejlsberg: 泛型的本质就是让你的类型能够拥有类型参数。它们也被称为参数化类型(parameterized types)或者参数的多态(parametric polymorphism)。经典的例子就是一个List集合类。List是一个方便易用的、可增长的数组。它有一个排序方法,你可以通过索引来引用它的元素,等等。现今,如果没有参数化类型,在使用数组或者Lists之间就会有些别扭的地方。如果使用数组,你得到了强类型保证,因为你可以定义一个关于Customer的数组,但是你没有可增长性和那些方便易用的方法。如果你用的是List,虽然你得到了所有这些方便,但是却丧失了强类型保证。你不能指定一个List是关于什么的List。它只是一个关于Object的List。这会给你带来一些问题。类型检测必须在运行时刻做,也就意味着没有在编译时刻对类型进行检测。即便是你塞给List一个Customer对象然后试图取出一个String,编译器也不会有丝毫的抱怨。直到运行时刻你才会发现他会出问题。另外,当把基元类型(primitive type)放入List的时候,还必须对它们进行装箱(box)。基于上述所有这些问题,Lists与Arrays之间的这种不和谐的地方总是存在的。到底选择哪个,会让你一直犹豫不决。
泛型的最大好处就是它让你有了一个两全其美的办法(you can have your cake and eat it too),因为你可以定义一个List<T>[读作:List of T]。当使用一个List的时候,你可以实实在在地知道这个List是关于什么类型的List,并且让编译器为你做强类型检测。这只是它最直接的好处。接下来还有其它各种各样的好处。当然,你不会仅仅想让List拥有泛型。哈希表(Hashtable)或者字典(Dictionary)——随便你怎么叫它——把键(keys)映射到值(values)。你可能会想要把Strings映射到Customrs,或者ints到Orders,而且是以强类型化的方式。
C#的泛型
Bill Venners: 泛型在C#中是如何工作的?
Anders Hejlsberg: 没有泛型的C#,基本上你只能写class List {...}。有了泛型,你可以写成class List<T> {...},这里T是类型参数。在List<T>范围内你可以把T当作类型来使用,当真正需要创建一个List对象的时候,写成List<int>或者List<Customer>。新类型是通过List<T>构建的,实际上就像是你的类型参数替换掉了原本的类型参数。所有的T都变成了ints或者Customers,你不需要做类型转换,因为到处都会做强类型检验。
在CLR(Common Language Runtime)环境下,当编译List<T>或者其它任何generic类型的时候,会像其它普通类型一样,先编译成中间语言IL(Intermediate Language)以及元数据。理所当然,IL以及元数据包含了额外的信息,从而可以知道有一个类型参数,但是从原则上来说,generic类型的编译与其它类型并没有什么不同。在运行时刻,当应用程序第一次引用到List<int>的时候,系统会查找看是否有人已经请求过List<int>。如果没有,它会把List<T>的IL和元数据以及类型参数int传递给JIT。而JITer在即时编译IL的过程中,也会替换掉类型参数。
Bruce Eckel: 也就是说它是在运行时刻实例化的。
Anders Hejlsberg: 的确如此,它是在运行时刻实例化的。它在需要的时候产生出针对特定类型的原生代码(native code)。从字面上看,当你说List<int>的时候,你会得到一个关于int的List。如果generic类型的代码使用了一个关于T的array,你得到的就是一个关于int的array。
Bruce Eckel: 垃圾回收机制会在某个时候来回收它么?
Anders Hejlsberg: 可以说会,也可以说不会,这是一个正交的问题。这个类在应用程序范围内被创建,然后在这个应用程序范围内就一直存在下去。如果你杀掉这个应用程序,那么这个类也就消失了,这点跟其它类一样。
Bruce Eckel: 如果我有一个应用程序用到了List<int>和List<Cat>,但是它从来没有走到使用List<Cat>的那个分支。。。。。。
Anders Hejlsberg:。。。。。。那么系统就不会实例化一个List<Cat>。现在让我说说一些例外的情况。如果你是使用NGEN在创建一个影像(image),也就是说你在直接产生一个native的映像,你可以提早产生这些实例。但是如果你是在通常的情况下运行程序,是否实例化是完全根据需要来确定的,而且推迟到越晚越好。
这之后,我们针对所有值类型(比如List<int>,List<long>,List<Double>, List<float>)的实例化做进一步的处理,创建可执行的原生代码的唯一拷贝。这样List<int>就有它自己的代码。List<long>也有它自己的代码。List<float>也是如此。对于所有引用类型(reference types),我们共享这些代码,因为它们所代表的东西是相同的。它们只是一些指针罢了。
Bruce Eckel: 你需要进行类型转换吧。
Anders Hejlsberg: 不,实际上并不需要。我们可以共享native image,但实际上它们有各自单独的虚函数表(VTables)。我只是想指出,当共享代码有意义的时候,我们会不遗余力的去做这件事情,但是当你非常需要运行效率的时候,我们对于共享代码会非常谨慎。通常对于值类型,你确实会关心List<int>元素的类型就是int。你不想把它们装箱(box)成Objects。对值类型进行装箱/拆箱,是可以用来进行代码共享的一种方法,但是这种方法代价过于昂贵。
Bill Venners: 对于引用类型,实际上也是完全不同的类。List<Elephant>和List<Orangutan>是不同的,但是它们确实共享所有的类方法的代码。
Anders Hejlsberg: 是的。作为实现上的细节来说,它们确实共享了相同的原生代码(native code)。
C#泛型与Java泛型的比较
Bruce Eckel: C#泛型相比Java泛型有什么特点?
Anders Hejlsberg: Java的泛型实现是基于一个最初叫做Pizza的项目,这个项目是由Martin Odersky和其他一些人完成的。Pizza被重新命名为GJ,然后他成了一个JSR,并且最后被采纳进了Java语言。这个特定的泛型proposal有一个关键的设计目标,就是它应该能够跑在不必经过改动的虚拟机上。不用改动虚拟机当然很棒,但是它也带来了一系列奇奇怪怪的限制。这些限制并不都是显而易见的,但是很快你就会说,“Hmm,这可有点怪。”
比如说,使用Java泛型,实际上你就得不到任何刚才我所说得程序执行上的效率,因为当你在Java里编译一个泛型类的时候,编译器拿掉了类型参数并到处代之以Object。List<T>编译好的影像文件(image)就像是一个到处使用Object(作为类型参数)的List。当然,如果你试图创建一个List<int>,那就的对所有用到的int对象进行装箱(boxing)。这就产生了很大的负担。此外,为了与老的虚拟机兼容,编译器实际上会插入各种各样的转换代码,而这些转换代码并不是由你来写的。如果是一个关于Object的List,而你试图把这些Objects当作Customers来对待,这些Objects必须在某些地方被转换成Customers,以便让verifier的验证能够通过。实际上它们的实现所做的就是自动为你插入那些类型转换。也就是说你得到了语法上的甜头,或者至少是一部分语法上的甜头,但是你并没有得到任何程序执行上的效率。这是我认为Java泛型解决方案的第一个问题。
第二个问题是,我认为这可能是更大的一个问题,因为Java的泛型实现依赖于去处掉类型参数,当到了运行时刻,你实际上并没有一个相对于运行时刻的可靠的泛型表示。当你在Java里针对一个泛型List使用反射(reflection)的时候,你并不知道这个List到底是关于什么的List。它只是一个List。因为你已经丢失了类型信息,对于任何动态代码生成(dynamic code-generation)的应用或者基于反射的应用,就没法工作了。这种趋势对我来说已经很明了了,(丢失类型信息的)情况越来越多。它根本没办法工作,因为你丢失了类型信息。而在我们的实现里,所有这些信息都是可获得的。你可以通过反射得到List<T>对象的System.Type表示。但这时候你还不能创建它的实例,因为你还不知道T是什么。但是你可以使用反射得到int的System.Type表示。然后你可以请求反射机制把这两个东西放在一起创建一个List<int>,这样你就得到了另外一个用以表示List<int>的System.Type。也就是说,从表示方法来说,任何你可以在编译时刻做到的事情,你也可以在运行时刻做到。
C#泛型与C++模板的比较
Bruce Eckel: C#泛型相比C++模板有哪些特点?
Anders Hejlsberg: 在我看来,理解C#泛型与C++模板之间的差异最重要的一点就是:C#泛型实际上就像是类,除了它们有类型参数。而C++模板实际上就像是宏(macros),除了它们看起来像是类。
C#泛型与C++模板最大的不同之处在于类型检验发生的时间以及实例化的方式。首先,C#是在运行时刻实例化的,而C++ 是在编译时刻或者可能是在link的时候。但是不管怎样,C++模板实例化发生在程序运行之前。这是第一个不同之处。第二个不同之处在于,当你编译generic类型的时候,C#对它进行强类型检验。对于像List<T>这样未加限制的类型参数(unconstrained type parameter),类型T的值所能使用的方法仅限于Object类型所包含的方法,因为只有这些方法才是通常我们保证能够存在的方法。也就是说,在C#泛型里,我们保证你所实施于类型参数的任何操作都会成功。
C++正好与此相反。在C++里,你可以对一个类型参数做任何你想做的事情。但是当你对它进行实例化的时候,它有可能通不过,而你会得到一些非常难懂的错误信息。比如,你有一个类型参数T以及两个T类型的变量,x和y,如果你写成x+y,那你最好事先定义了用于两个T型变量相加的+运算符,否则你会得到一些古怪的错误信息。所以从某种意义上说,C++模板实际上是非类型化的,或者说是弱类型化的。而C#泛型则是强类型化的。
C#泛型的constraints特性
Bruce Eckel: constraints在C#泛型里是如何工作的?
Anders Hejlsberg: 在C#泛型里,我们可以针对类型参数加一些限制条件(constraints)。还以List<T>为例,你可以写成,class List<T> where T: IComparable。意思是T必须实现IComparable接口。
Bruce Eckel: 有意思的是在C++里限制条件是隐含的。
Anders Hejlsberg: