摘要
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以及互操作的重要性。
在第六部分:Inappropriate Abstractions里, Hejlsberg以及C#团队的其他成员谈论了试图让网络透明的分布式系统,以及试图屏蔽掉数据库的对象——关系映射。
在第七部分, 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
C#的泛型
Bill Venners: 泛型在C#中是如何工作的?
Anders Hejlsberg: 没有泛型的C#,基本上你只能写class List {...}。有了泛型,你可以写成class List
在CLR(Common Language Runtime)环境下,当编译List
Bruce Eckel: 也就是说它是在运行时刻实例化的。
Anders Hejlsberg: 的确如此,它是在运行时刻实例化的。它在需要的时候产生出针对特定类型的原生代码(native code)。从字面上看,当你说List
Bruce Eckel: 垃圾回收机制会在某个时候来回收它么?
Anders Hejlsberg: 可以说会,也可以说不会,这是一个正交的问题。这个类在应用程序范围内被创建,然后在这个应用程序范围内就一直存在下去。如果你杀掉这个应用程序,那么这个类也就消失了,这点跟其它类一样。
Bruce Eckel: 如果我有一个应用程序用到了List
Anders Hejlsberg:。。。。。。那么系统就不会实例化一个List
这之后,我们针对所有值类型(比如List
Bruce Eckel: 你需要进行类型转换吧。
Anders Hejlsberg: 不,实际上并不需要。我们可以共享native image,但实际上它们有各自单独的虚函数表(VTables)。我只是想指出,当共享代码有意义的时候,我们会不遗余力的去做这件事情,但是当你非常需要运行效率的时候,我们对于共享代码会非常谨慎。通常对于值类型,你确实会关心List
Bill Venners: 对于引用类型,实际上也是完全不同的类。List
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
第二个问题是,我认为这可能是更大的一个问题,因为Java的泛型实现依赖于去处掉类型参数,当到了运行时刻,你实际上并没有一个相对于运行时刻的可靠的泛型表示。当你在Java里针对一个泛型List使用反射(reflection)的时候,你并不知道这个List到底是关于什么的List。它只是一个List。因为你已经丢失了类型信息,对于任何动态代码生成(dynamic code-generation)的应用或者基于反射的应用,就没法工作了。这种趋势对我来说已经很明了了,(丢失类型信息的)情况越来越多。它根本没办法工作,因为你丢失了类型信息。而在我们的实现里,所有这些信息都是可获得的。你可以通过反射得到List
C#泛型与C++模板的比较
Bruce Eckel: C#泛型相比C++模板有哪些特点?
Anders Hejlsberg: 在我看来,理解C#泛型与C++模板之间的差异最重要的一点就是:C#泛型实际上就像是类,除了它们有类型参数。而C++模板实际上就像是宏(macros),除了它们看起来像是类。
C#泛型与C++模板最大的不同之处在于类型检验发生的时间以及实例化的方式。首先,C#是在运行时刻实例化的,而C++ 是在编译时刻或者可能是在link的时候。但是不管怎样,C++模板实例化发生在程序运行之前。这是第一个不同之处。第二个不同之处在于,当你编译generic类型的时候,C#对它进行强类型检验。对于像List
C++正好与此相反。在C++里,你可以对一个类型参数做任何你想做的事情。但是当你对它进行实例化的时候,它有可能通不过,而你会得到一些非常难懂的错误信息。比如,你有一个类型参数T以及两个T类型的变量,x和y,如果你写成x+y,那你最好事先定义了用于两个T型变量相加的+运算符,否则你会得到一些古怪的错误信息。所以从某种意义上说,C++模板实际上是非类型化的,或者说是弱类型化的。而C#泛型则是强类型化的。
C#泛型的constraints特性
Bruce Eckel: constraints在C#泛型里是如何工作的?
Anders Hejlsberg: 在C#泛型里,我们可以针对类型参数加一些限制条件(constraints)。还以List
Bruce Eckel: 有意思的是在C++里限制条件是隐含的。
Anders Hejlsberg: 是的。在C#里,你也可以让限制条件是隐含的。比如说我们有一个Dictionary
使用constraint,你可以把代码里的动态检验提前,在编译时刻或者加载的时候对它进行验证。当你指定K必须实现IComparable,这就隐含了一系列的东西。对于任何K类型的值,你都可以直接访问接口方法,而不需要进行转换,因为从语义上来说,在整个程序里K类型要实现这个接口,这一点是得到保证的。无论什么时候你想要创建该类型的一个实例,编译器都会针对你给出的任何作为K参数的类型进行检验,看它是否实现了IComparable。如果没有实现,你会得到一个编译时错误。或者如果你是利用反射来做的话,会得到一个异常。
Bruce Eckel: 你说到了编译器以及运行时刻。
Anders Hejlsberg: 编译器会做检验,但是你也可能是在运行时刻通过反射来做的,这时候就由系统来做检验。如前所述,任何你在编译时刻可以做的事情,你都可以在运行时刻通过反射来做。
Bruce Eckel: 我是否可以写一个模板函数,或者换句话说,一个参数类型未知的函数?你们是在所做的是给容器加上更强的类型检验,但是我是否可以像在C++模板里那样得到弱类型化的东西呢?比如说,我是否可以写一个函数,它以A a和B b作为参数,然后我在代码里就可以写a+b?我是否可以不关心A和B是什么,只要它们有一个“+”运算符就可以了,因为我想要的是弱类型化。
Anders Hejlsberg: 你实际上问的是,通过constraints你到底能做到什么程度?与其它特性类似,如果把constraints发挥到极致,他可以变得异常复杂。仔细想想,其实constraints是一种模式匹配(pattern matching)的机制。你想要能指定,“该类型参数必须有一个接受两个参数的构造函数,并且实现了+运算符,要有某个静态方法,以及其它两个非静态方法,等等。”问题是,你想要这种模式匹配的机制复杂到哪种程度?
从什么也不做到功能全面的模式匹配,这是很大的一个范围。我们认为什么也不做太说不过去了,而全面的模式匹配又会变得非常复杂,所以我们选择了折衷的方式。我们允许你指定一个constraint,它可以是一个类、零个或者多个接口、以及叫做constructor constraint的东西。比如说,你可以指定“该类型必须实现IFoo和IBar接口,”或者“该类型必须继承自基类X。”一旦你这么做了,我们会在所有地方做类型检验以确认该constraint是否为真,包括编译时刻和运行时刻。任何由这个constraint所暗含的方法都可以通过类型参数的实例直接访问。
另外,在C#里,运算符都是静态成员函数。也就是说,一个运算符永远不可能成为一个接口的成员函数,因此一个接口限制条件(interface constraint)永远不可能让你指定一个“+”运算符。要指定一个“+”运算符,唯一的方法就是通过一个类限制条件(class constraint),这个类限制条件指定说必须继承自某个类,比如说Number类,因为Number有一个“+”运算符。但是你不可能把它抽象成:“必须有一个+运算符”,然后由我们来以多态的方式解析它的实际含义。
Bill Venners: 你是通过类型,而不是签名(signature)来实现限制条件的。
Anders Hejlsberg: 是的。
Bill Venners: 也就是说指定类型必须扩展某个类或者实现某些接口。
Anders Hejlsberg: 是的。本来我们可以走得更远。我们确实考虑过走得更远一些,但是那会非常复杂。并且我们不知道添加这些复杂性相对于你所获得的微不足道的好处,是否值得。如果你想做的事情没有被constraint系统直接支持,你可以借助于工厂模式(factory pattern)来完成。比如说,你有一个矩阵类Matrix
Bruce Eckel: Calculator也是个参数化类型。
Anders Hejlsberg: 是的,它有点像factory模式。总之,是有办法来做这些事情的。可能不如你想要的那么棒,但是任何事情都是有代价的。
Bruce Eckel: 嗯,我感觉C++模板像是一种弱类型化(weak typing)的机制。当你开始在它上面添加constraints的时候,你是在从弱类型化转向强类型化(strong typing)。通常加入强类型化都会让事情更加复杂。这像是一个频谱。
Anders Hejlsberg: 你所意识到的类型化(typing)的问题,其实是一个拨盘(dial)。你把它拨的越高,程序员越觉得难受,但同时代码更安全了。但是在两个方向上你都有可能把它拨过头。
<> <>