摘要
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<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: 是的。在c#里,你也可以让限制条件是隐含的。比如说我们有一个dictionary<k,v>,它有一个add方法,以k为键(key)v为值(value)。add方法的实现很可能需要把传入的键与dictionary已有的键进行比较,而且它可能通过一个叫做icomparable的接口来做这个比较。一种方法是把key参数转换成icomparable,然后调用compareto方法。当然,当你这么做的时候,你就已经针对k类型和key参数创建了一个隐式的限制条件。如果传入的key没有实现icomparable接口,你就会得到一个运行时错误。但是实际上你并没有在你的哪个方法里或者约定里明确表明key必须实现icomparable。而且你当然还得付出运行时刻类型检测的代价,因为实际上你所做的是运行时刻的动态类型检验。
使用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<t>,在这个matrix里你想定义一个标量积(dot product)方法。这当然意味着你最终需要理解如何把两个t相乘,但你不能把它表达成一个constraint,至少如果t是int、double或者float的时候这样做不行。但是你可以这么做:让matrix接受一个calculator<t>这样的参数,然后在calculator<t>里声明一个叫做multiply的方法。你实现这个方法并把它传给matrix。
bruce eckel: calculator也是个参数化类型。
anders hejlsberg: 是的,它有点像factory模式。总之,是有办法来做这些事情的。可能不如你想要的那么棒,但是任何事情都是有代价的。
bruce eckel: 嗯,我感觉c++模板像是一种弱类型化(weak typing)的机制。当你开始在它上面添加constraints的时候,你是在从弱类型化转向强类型化(strong typing)。通常加入强类型化都会让事情更加复杂。这像是一个频谱。
anders hejlsberg: 你所意识到的类型化(typing)的问题,其实是一个拨盘(dial)。你把它拨的越高,程序员越觉得难受,但同时代码更安全了。但是在两个方向上你都有可能把它拨过头。
新闻热点
疑难解答
图片精选