(收藏)Anders Hejlsberg谈C#、Java和C++中的泛型

Anders Hejlsberg谈C#、Java和C++中的泛型

原著:Bill Venners、Bruce Eckel  2004.2.26
原文http://www.artima.com/intv/generics.html翻译:lover_P
出处http://www.cstc.net.cn/docs/docs.php?id=258


[人物介绍]

    Anders Hejlsberg,微软著名工程师,带领他的小组设计了C#(读作:C-Sharp)程序设计语言。Hejlsberg第一次登上软件界历史舞台是在80年代早期,因为他为MS-DOS和CP/M设计了Pascal编译器。当时,还是一个小公司的Borland很快雇用了他,并买下了他的编译器,改称Turbo Pascal。在Borland,Hejlsberg继续开发Turbo Pascal,并最终带领他的小组设计了Turbo Pascal的替代品:Delphi。1996年,在进入Borland 13年后,Hejlsberg加入了微软。最初,他做Visual J++和Windows Fundatioin Classes(WFC)的架构师。随后,Hejlsberg成为C#的首席设计师和.NET Framework的关键参与者。目前,Anders Hejlsberg还在领导着C#程序设计语言的继续开发。

    Bruce Eckel,Think in C++(C++编程思想)和Think in Java(Java编程思想)的作者。

    Bill Venners,Artima.com的主编。

[内容]

泛型概述

    Bruce Eckel:您能对泛型做一个快速的介绍么?

    Anders Hejlsberg:泛型其实就是能够向你的类型中加入类型参数的一种能力,也称作参数化的类型或参数多态性。最著名的例子就是List集合类。一个List是一个易于增长的数组。它有一个排序方法,你可以为 它做索引,等等。现在,如果没有参数化的类型,那么不论使用数组还是使用List都不是很好。如果你使用数组,你能获得强类型,因为你可以声明一个Customer类型的数组,但你失去了可增长性和那些方便的方法;如果你使用一个List,你能够得到所有的便利,但你失去了强类型。你难以说出一个List是什么(类型的)List,它只是一个Object的List【译注:“什么类型的List”指的是List存放的元素是什么类型的】。这会给你带来麻烦 ,因为类型只能在运行形时进行检查,也就是说在编译时不会进行类型检查。就算你硬要把一个Customer放进一个List并试图从中得到一个String,编译器也不会不高兴。在运行之前你根本无法发现它不能工作。同时,当你将简单类型【译注:指值类型】放入List时,还必须对它们进行装箱。正是由于这些问题,你不得不在List和数组之间徘徊,你经常要很痛苦地决定应该使用哪一个。

    泛型的伟大之处在于你现在可以尽情地享受你的蛋糕了,因为你能够定义一个List<T>(读作:List of T)【译注:中文可以说成“T类型的List”】。当你使用List时,你居然能够说出它是什么类型的List,并且你将获得强类型,编译器会为你检查它的类型。这些只是直觉上的好处,它还有其它许多优点。当然,你并不是只能将它用于List,Hastable、Dictionary(将键影射到值上的数据结构)——所有你想调用的都行。你可能想将String影射到Customer、将int影射到Order,在这些情况下你都能获得强类型。

C#中的泛型

    Bill Venners:泛型在C#中是如何工作的呢?

    Anders Hejlsberg:在没有泛型的C#中,你只能写class List {...};而在带有泛型的C#中,你可以写class List<T> {...},这里的T是一个类型参数。在List<T>中,你可以把T就当作一个类型来用。当它实际用来建立一个List对象时,你要写List<int>或List<Customer>。这样你就从List<T>构造了一个新的类型,看起来就好像你用你的类型变量替换了所有的类型参数。所有的T都变成了int或Customer,你无须进行向下转换,它们是强类型的,任何时候都会被检查。

    在CLR(Common Language Runtime,公共语言运行时)中,当你编译List<T>或其它泛型类型时,它们和普通类型一样被转换为IL(Intermediate Language,中间语言)和元数据。IL和元数据带有附加信息,可以知道这是一个类型参数,当然,原则上泛型类型的编译和其它类型一样。在运行时,当你的应用程序第一次引用List<T>时,系统会看看你是否已经使用过List<int>。如果没有,它会调用JIT将带有int类型变量的List<T>编译为IL和元数据。当JIT即时编译IL时,同样会替换类型参数。

    Bruce Eckel:所以它是在运行时被实例化的。

    Anders Hejlsberg:它确实是在运行时实例化。它在需要的时候才产生特定的原生代码(native code)。字面上,当你说List<T>时,你会得到一个int类型的List。如果泛型类型中使用的是T类型的数组,它会变成int类型的数组。

    Bruce Eckel:这个类会在某一时刻被垃圾收集器收集么?

    Anders Hejlsberg:是也不是,这是一个正交的问题。它会在该程序集中建立一个类,这个类在程序集中会一直存在。如果你终止了程序集,这个类会消失,和其它类一样。

    Bruce Eckel:但如果我的程序中声明了一个List<int>和一个List<Cat>,但我从未使用过List<Cat>……

    Anders Hejlsberg:……那么系统不会实例化List<Cat>。当然,下面的情况除外。如果你使用NGEN产生一个镜像,也就是说如果你预先生成了一个原生代码的镜像,会预先实例化。但是如果你在一般的环境下运行,则这个实例化是纯需求驱动(demand driven)的,会尽可能地延迟【译注:正如上面所说,直到使用时才进行实例化】。

    实际上,我们所要进行实例化的所有类型都是值类型——如List<int>、List<long>、List<double>、List<float>——我们为每一个都建立一份唯一的可执行原生代码的拷贝。因此,List<int>有它自己的代码,List<long>有它自己的代码,List<float>有它自己的代码。对于所有的引用类型我们共享它们的代码,因为它们在表现上是一样的,它们只是一些指针。

    Bruce Eckel:因此你只需要转换。

    Anders Hejlsberg:不,实际上是不需要的。我们可以共享原生镜像,但他们实际上具有独立的VTable。我要指出的是,我们只是尽量对代码进行有意义的共享,但我们很清楚,为了效率,有很多代码是不能共享的。典型的就是值类型,你会很关心List<int>中到底是不是int。你肯定不希望将它们被装箱为Object。对值类型进行装箱是一种共享的方法,但对它们进行装箱开销会很大。

    Bill Venners:对于引用类型,所不同的只是类。List<Elephant>不同于List<Orangutan>,但他们实际上共享了所有方法的代码。

    Anders Hejlsberg:是的。作为实现的细节,它们实际上共享了相同的原生代码。

C#泛型和java泛型的比较

    Bruce Eckel:如何比较C#中的泛型和java中的泛型呢?

    Adners hejlsberg:Java的泛型最初是基于Martin Odersky和其它人一起做的称作Pizza的一个项目的。Pizza后改名为GJ,然后成为JSR,最后以被Java语言收容而告终。这种泛型以能够在原有的VM(Virtual Machine,虚拟机)上运行为关键设计目标。也就是说,你不必修改你的VM,但它会带来很多限制。这些限制并不会很快出现,但很快你就会说:“嗯,这有点陌生。”

    例如,使用Java泛型,我觉得你实际上不会获得任何的执行效率,因为当你编译一个Java泛型类时,编译器会将所有的类型参数替换为Object。当然,如果你尝试建立一个List<int>,你就需要对所有的int进行装箱。因此,这会有很大的开销。另外,为了让VM高兴,编译器必须为所有的类型插入类型转换。如果一个List是Object的,而你想将这些Object视为Customer,就必须将Object转换为Customer,以让类型检查器满意。而它在实现这些的时候,真的只是为你插入所有这些类型转换。因此,你只是尝到了语法上的甜头,却没有获得任何执行效率。所以我觉得这是(泛型的)Java实现的头号问题。

    第二号问题,我觉得也是一个很严重的问题,这就是由于Java泛型是依靠消除所有的类型参数来实现的,你就无法在运行时获得一个和编译时同样可靠的表现。当你在Java中反射一个泛型的List的时候,你无法得知这是个List什么类型的List。它只是一个List。因为你失去了类型信息,任何由代码生成方案或基于反射的方案所产生的动态类型都将无法工作。唯一让我认为清晰的趋势就是,越来越多的东西将不能运行,就是因为你丢掉了类型信息。但在我们的实现中,所有这些信息都是可用的。你可以使用反射来获得List<T>对象的System.Type。但你还不能建立它的一个实例,因为你并不知道T是什么。但是接下来你可以使用反射来获得int的Sytem.Type。然后你就可以请求反射将这两个System.Type结合起来并建立一个List<int>,然后你还能获得List<int>的另一个System.Type。因此,所有你在编译期间能做的在运行时同样可以。

C#泛型和C++模板的比较

    Bruce Eckel:如何比较C#泛型和C++模板呢?

    Anders Hejlsberg:我认为对C#泛型和C++模板之间的区别最好的理解是:C#泛型更像类,只不过它带有类型参数;C++模板接近宏,只不过它看起来像类。

    C#泛型和C++模板之间最大的区别在于类型检查发生的时机和如何进行实例化。首先,C#在运行时进行实例化。而C++在编译时,或者可能是连接时进行实例化。不管怎么说,C++是在程序运行前进行实例化。这是第一点不同。第二点不同是当你编译泛型类型时,C#会进行强类型检查。对于一个非约束的类型参数,如List<T>,能够在类型为T的值上执行的方法仅仅是那些能够在Object类型中找到的方法,因为只有这些方法是我们能够保证存在的。在C#中,我们要保证在一个类型参数上执行的所有操作都能成功。

    C++正相反。在C++中,你可以在类型参数所指定的类型的变量上执行你想做的任何操作。但是一旦你对它进行了实例化,它就有可能无法工作,你将会得到一些含义模糊的错误信息。例如,如果你有一个类型参数T,而x和y是T类型的变量,然后你执行x+y,如果你对两个T定义了一个operator+还好说,否则你就只能得到一些没意义的错误消息。因此,从某种意义上说,C++模板实际上是无类型的,或者说是弱类型的。而C#泛型是强类型的。

C#泛型中的约束

    Bruce Eckel:约束是如何在C#泛型中工作的呢?

    Anders Hejlsberg:在C#泛型中,我们能够为类型参数施加约束。以我们的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()方法的实现将希望能够将传递进来的key和Dictionary中已经存在的key进行比较,而且它希望使用一个称作IComparable的接口。唯一的途径就是将key参数转换为IComparable接口,然后调用CompareTo方法。当然,当你这么做的时候,你就为K类型和key参数建立了一个隐式的约束。如果传递进来的key没有实现IComparable接口,你会得到一个运行时错误。这在你的所有方法中都有可能出现,因为你的约定没有要求key必须实现IComparable接口。当然,你还得为运行时类型检查付出代价,因为你实际上进行了动态类型转换。

    使用约束,你可以消除代码中的动态检查,而在编译时或装载时进行。当你要求K必须实现IComparable接口时,会发生很多事情。对于K类型的值,你现在可以直接访问接口方法而无需类型转换。因为程序在语义上可以保证它实现了这个接口。无论什么时候你尝试建立这个类型的一个实例时,编译器都会检查这些类型是否实现了这个接口,如果没有实现,会给你一个编译错误。如果你使用的是反射,你会得到一个异常。

    Bruce Eckel:你是说编译器和运行时(都会进行检查)?

    Anders Hejlsberg:编译器会检查它,但你仍有可能在运行时通过反射来做这些,因此系统还会检查它。正像我前面说的,编译时可以做的任何事都可以在运行是通过反射来做。

    Bruce Eckel:我可以做一个函数模板,换句话说,一个带有不知道类型的参数的函数?你为约束添加了强类型检查,但我是不是能像C++模板那样得到一个弱类型模板? 例如,我能否写一个函数,它带有两个参数A a和B b,并在代码中写a+b?我能不能说我不在乎对于A和B是否有operator+,因为它们是弱类型的?

    Anders Hejlsberg:你真正要问的问题应该是这在约束中如何说吧?约束,和其他特性一样,最终将可以是任意复杂的。当你考虑它的时候,约束只是一个模式匹配机制。你可能希望能够说“这个类型参数必须有一个带有两个参数的构造器、实现了operator+、有这个静态方法、有那两个实例方法、等等”。问题是,你希望这种模式匹配机制有多复杂?

    从没有任何东西到完全模式匹配是一个整个的连续体。没有任何东西(的模式匹配)太小了,不能说明问题;而完全模式匹配又太复杂了,因此我们需要在中间找一个平衡点。我们允许你将约束指定为一个类、一个或多个接口,以及一些构造器约束。譬如,你可以说:“这个类型必须实现IFoo和IBar”或“这个类型必须继承基类X”。一旦你这么做了,在编译时和运行时都会检查这个约束是否为真。这个约束所隐含的任何方法对于类型参数所指定的类型的值都是直接有效的。

    现在,在C#中,运算符是静态成员。因此,运算符不能是接口的成员,因此接口约束不能带给你operator+。你只能通过类约束获得operator+,你可以说这个类型参数必须继承自比如说Number类,并且Number类对于两个Nubmer有operator+。但你不能抽象地说“必须有一个operator+”,我们无法知道这句话的具体含义。

    Bill Venners:你通过类型进行约束,而不是签名。

    Anders Hejlsberg:是的。

    Bill Venners:因此这个类型必须扩展一个类或实现一个接口。

    Anders Hejlsberg:是的。而且我们还能够走得更远。实际上我们也想过再走远一些,但这会变得相当复杂。而且增加的复杂性与所得到的相比很不值得。如果你想做的事情在约束系统中不直接支持,你可以使用一个工厂模式。例如你有一个Martix<T>,而在这个Martix(矩阵)中,你可能想定义一个“点乘”【译注:矩阵上的一种乘法运算,另一种称为“叉乘”】方法。这意味着你最终将要考虑如何将两个T相乘,但你不能将这说成是一个约束,至少当T不是int、double或float时你不能这么说。但你可以让你的Martix带有一个Calculator<T>作为参数,而在Calculator<T>中,有一个称为Multiply的方法。你可以在其中进行实现,并将结果传递给Martix。

    Bruce Eckel:而且Calculator也是一个参数化的类型。

    Anders Hejlsberg:是的。这有些像工厂模式,还有很多方法可以做到,这也许不是你最喜欢的方法,但做任何事情都要付出代价。

    Bruce Eckel: 是呀,我开始认为C++模板是一种弱类型机制。而当你想其中添加了约束后,你从弱类型走向了强类型。但这一定会带来更多的复杂性。这就是代价吧。

    Anders Hejlsberg: 关于类型你可以认为它是一个标尺。这个标尺定得越高,程序员的日子就会越不好过,但更高的安全性随之而来。但你可以把这个标尺向任何一个方向调节。

posted @ 2006-01-26 13:53  81  阅读(625)  评论(1编辑  收藏  举报