Thinking in Delphi:语言的变革
Delphi Language的诞生
2003年11月,Borland公司正式发布了Delphi的最新版 本:Delphi 8 for Microsoft .NET Framework。如它的名字所揭示的,Delphi 8不再支持Win32平台下的开发,而是完全基于Microsoft .NET Framework。毫无疑问,这为Delphi带来了革命性的变化,而让一个计算机编程语言的爱好者最感兴趣的地方,则莫过于Delphi 8对语言本身的改进。
众所周知,Delphi将OO的思想带入传统的Pascal中,从而创造了著名的Object Pascal语言。从Delphi 1以来,Object Pascal一直在不断的改进中。但是在从Delphi 7升级到Delphi 8的过程中,不但编程模型发生了根本性的变革,语言本身也得到了大幅度的扩展和改进,结果就是导致了几乎是一种新的语言的诞生。在本文中,将Delphi 8中使用的语言称为Delphi Language,与以前的Object Pascal相区别。
从Delphi 7开始,Borland就已经将Object Pascal称为Delphi Language。但是Delphi 7中的语言与以前版本相比并没有根本性的改变,这一改变实际上是发生在Delphi 8中的。因此,在本文中,将Delphi 8中使用的语言称为Delphi Language,以前版本的Delphi中使用的语言则称为Object Pascal。
Delphi Language增加了对如下特性的支持:名字空间(Namespace)、嵌套类(Nested Type)、类静态方法(Class Static Method)和类属性(Class Property)、对记录(Record)类型的增强、密封类(Sealed Class)和Final方法(Final Method)、多播事件(Multicast Events)机制、运算符重载(Operators Overload)、装箱/拆箱(Boxing/Unboxing)机制,等等。
不同的编程语言之间的比较是一件非常容易带来争议的事情。但是,在许多情况下,这种比较看起来似乎是不可避免的。尤其是在编程语言领域出现革新时,语言之间的横向比较往往能够让人更清楚地看到这种革新的本质。因此,当Delphi Language面世时,将它与其它的主流编程语言进行比较就成为了一种诱惑。在下文中,作者将进行这种比较。
与Delphi Language进行比较的语言的挑选标准是这样的:
- 必须是通用的语言而不是专用的语言。
- 必须是强类型的、编译或半编译的语言,而不是解释执行的脚本语言。
- 必须是完全支持面向对象编程(OOP)的语言(这样就将C这样的语言排除在外,虽然C现在仍然是最主流的工业语言标准之一)。
这种挑选的方式也许并不合适,但是,这种比较仅仅表明作者在某个特定的角度进行观察时得到的结论,而不是试图对这些语言作出优劣之分。而且,相信许多读者也会赞成这一点,即将应用范围和实现方式完全不同的语言进行比较,在很大程度上是没有意义的。根据这个标准,C++、Java、C#成为中选者,分别代表了Java、.NET和原生代码(Native Code)三个主要平台下的编程语言。Visual Basic .NET由于和C#过于相似,作者认为没有将它增加进来的必要。Delphi Language的前身Object Pascal也被加入以作为参照。
语言特征
诚如有人指出过的,语言之间的比较很容易受到主观的影响,因此作者尽量将比较的范围限制于实证性的语言特征的层次上,对于这些问题基本上不会存在多少争议。表1列出了这几种语言的比较,Y表示是,N表示否。
表1 实特征的语言特征对照表
注1:Java通过包裹类(Wrapped Class)来完成基本类型和对象之间的转换,而.NET则在语法层次提供支持,并且这一功能内建在虚拟机的指令集中。因此,只能说Java部分地支持Boxing/Unboxing。但是,有消息说Java将在以后的版本中对Boxing/Unboxing机制提供类似.NET的支持。C++和Object Pascal则完全不支持Boxing/Unboxing。
注2:Sun公司已经宣布将在JDK 1.5中支持泛型编程(Generic Programming)。Microsoft也宣布将在下一个版本的C#中支持Generic Programming。关于这方面的讨论已经非常丰富。在2003年11月的Borland Conference上,Delphi的开发小组也宣布了类似的计划。
注3:Object Pacsal通过内置RTTI可以支持对不确定对象的属性和方法的检索,这也是Delphi组件机制的基础之一。但是Object Pacsal不支持动态生成类,因此只能说部分支持Reflection。
如同你可能想到的,这个表远远不够完备,即使在列出的这些特征中,有很多情况也不是仅仅用“是”或“否”可以总结的。这一点,从表后几个有点冗长的注解就可以看出来。
事实上,还有一些主要的区别没有在这张表中列出来。由于它们是如此复杂,以致于不可能简单地用“是”或“否”来放在一张表中,而必须专门进行讨论。
运行平台
从表面上看来,编程语言与运行其上的平台之间并没有必然的联系,但事实并非如此。下面是这几种语言和运行平台的对应关系:
表2
C++之父Bjarne Stroustrup博士说过一句非常有知名度的话:“Java并不是跨平台的,Java自己就是平台。”无论前一句的结论是否合适,后一句却是确凿无疑的。因此,在这里将JVM列为一个平台。按照同样的逻辑,Delphi Language和C#的运行平台是.NET CLR。
从上表中,我们可以发现这样一种现象,即Java和C#这两种纯粹的面向对象的语言都是基于虚拟机的。这就是说,这些运行于虚拟机之上的语言都不支持全局变量和全局方法。而Object Pascal和C++这两种多风格的编程语言则运行于原生平台之上。对这一现象进行思考以后,也许可以得出如下结论:现在所有的主流操作系统,包括Windows、Linux和Unix,底层都是由C或者汇编语言开发,提供的API都是C风格的。因此在原生平台上运行的编程语言必须支持C风格的全局变量和全局方法,而虚拟机却可以绕过这一限制,提供纯粹的面向对象的编程模型。
一个有趣的趋势是,Microsoft通过将操作系统从Win32向.NET的转移,正在逐渐地接近纯粹的面向对象的操作系统这一目标。
这一结论的一个重要的例外就是Delphi Language。它象Java和C#一样,运行于虚拟机之上,但是它仍然支持全局变量和全局方法,换句话说,仍然支持面向过程的编程风格。Delphi Language之所以提供这一特征,应该是为了向后兼容性考虑,因为毕竟有大量的Object Pascal代码在开发和维护中,它们可能需要过渡到.NET平台,而且存在着大量熟悉Object Pascal语法的程序员,他们希望以尽可能小的成本转移到.NET平台。
但是,这一现象的出现,说明了一个问题,那就是 .NET对编程语言的统一并没有人们猜想的那么严重。在.NET的CLS发布以后,人们曾经猜想.NET平台下所有的编程语言都长得一个样子。这一观点在Microsoft自己支持的4种语言(Visual Basic.NET、C#、Visual J#、Managed C++)中可能是正确的,但对于其它的语言就未必正确了。从理论上来说,任何语言只要满足CLS规定的最小规则集,就可以支持.NET,但CLS并没有限制这些语言进行的扩展,只要它们的编译器能够生成规范的 .NET代码即可。Delphi Language就是一个这样的例子,它在一个纯粹的面向对象的平台上保留了使用了面向过程编程的能力。另一个例子是Eiffel#,它通过强有力的编译器在.NET平台上实现了多重继承。
运行平台对编程语言的另一影响是内存自动回收(Garbage Collection)机制。JVM和.NET CLR这样的虚拟机都具有内存自动回收机制,因此对相应的Java、C#和Delphi Language这三种语言都产生了影响,尤其是在对象销毁方面。而运行于原生平台上的C++和Object Pascal则不具有这一机制,因此程序员必须自己实现内存的回收。是的,通过某些第三方库,C++和Object Pascal也可以实现内存自动回收,但这并不是语言本身的特征。
容器类
和容器(Container)打交道是开发人员日常编程中最经常遇到的工作,优秀的容器支持,对一种编程语言的推动作用,怎样估计也不会过分。
在C和Pascal的时代,除了数组(Array)这种最简单的容器之外,没有提供其他的支持。程序员必须自己实现象链表(List)这样的最基础的数据结构。
在Object Pascal中,这种情况得到了一些改善。Object Pascal提供了一些类作为容器,实现基本的数据结构,如TList是列表的实现,TQueue是队列的实现,TStack是栈的实现,TBucketList是哈希表(Hash Table)的实现。这些类将无类型指针而不是对象作为容器的元素,因此程序员必须自己负责进行类型转换和处理由此可能产生的问题,但另一个较为有益的后果是可以同时将基本类型和对象装入容器中,而无须进行Boxing/Unboxing的转换。从Delphi 6开始,增加了以对象为元素的容器类,例如TObjectList、TObjectQueue、TObjectStack和TObjectBucketList。在实际编程中,这些容器类非常具有实用性,但是,它们仍然只是一系列扁平的、不便于扩展的类,并不意味着一种基于面向对象思想的普遍的解决方案。
在C++中,对容器的支持得到了空前的改善。在模板(Template)和操作符重载(Operator Overload)技术的基础上,C++发展出了一整套标准模板库(Standard Template Library, STL)。STL提供了大量的容器类,既可以使用对象作为元素,也可以使用指针作为元素。同时,在函数对象(Function Object)技术的基础上,STL还提供了许多独立于容器类型的泛型算法,用于对容器中的元素进行各种操作。STL是如此强大和复杂,以至于出现了许多专门的著作对它进行讨论。但是,正因为如此,STL在方便开发工作的同时,也对开发者的水平提出了更高的要求。程序员们不得不花费大量的时间用于熟悉和使用STL,而事实上,大多数程序员在工作中遇到的需求,可能只占STL提供的功能的一小部分。
在Java中,效率和复杂程度达到了某种均衡。Java中的容器类完全以对象作为元素,在处理的方式上相当一致,但是由于容器类仅仅操纵Object对象,程序员仍然必须自己处理向上造型(Upcast)的问题。另外,基本类型必须通过编程转化为包裹类的对象,才能够被容器类所处理。除去这些问题以外,Java提供了一套规模适中、实用性极强的容器类。从Java 1.2开始,对以前版本的容器类进行了较大幅度的改进,最终形成了我们现在看到的这个样子。Java的容器类的规模、复杂程度和精巧性,正好使得程序员不需要象对待STL那样花费太多的时间进行学习和掌握,但又足以能够应付绝大多数常见的需求,不需要程序员象在C语言中那样频繁地重新实现自己的容器。实践证明,Java的容器类是非常成功的。
图一 Java的容器类
C#和Java一样,也提供了自己的容器类。目前C#的容器类的规模还无法与Java相比,但是它的风格与Java有明显的不同。这些容器类事实上是.NET的FCL中提供的。由于Delphi Language与C#使用同一套FCL,因此它的容器类与C#基本相同。
语言的演变
让我们回到表1,看看这张表格所列举的差异能够说明些什么。从时间顺序上来看,从C++,到Object Pascal,到Java,再到C#和Delphi Language,可以看出编程思想和风格(但并不表示优秀程度和实用性)的演化。
C++是这些语言中出现得最早的,同时它也提供了最好的向后兼容性:几乎全兼容面向过程编程时代的C语言。从市场策略来看,这是非常有远见的举动,C++成功地以C语言的后续者出现,但是同时也导致了许多严重的问题。对于这一点,C++的发明者也并不讳言。C++支持完全的面向对象编程,支持名字空间(Namespace),支持嵌套类型(Nested Type),这使得它对大规模程序的开发提供了很好的支持(这也是C++的设计目标之一)。但是,C++也保留了全局变量/全局函数、Structure等语法,这在获得兼容性的同时,牺牲了面向对象的特性(当然,C++的目的并不是纯粹的面向对象,而是多风格编程)。另外,C++在RTTI上完全无法与后来的几种语言相比。但是,C++还提供了复杂的模板和操作符重载机制,从而全面支持泛型编程(Generic Programming),这一点目前在这些语言中是独一无二的。总的说来,C++是一门庞大的、复杂的、博大精深的语言,它在从面向过程编程向面向对象编程的过渡中,最大限度地保持了向后兼容性,同时也付出了沉重的代价。C++在许多前沿领域,都进行了有益的探索,这些探索的成果被其它语言所汲取,但也使得C++的学习门槛过高。
Object Pascal是Anders Hejlsberg对传统的Pascal进行扩展以后发明的语言,它诞生的背景是面向对象编程思想的流行和Windows操作系统的崛起。针对这两个潮流,Object Pascal做了很多的优化以适应它们。
- Object Pascal支持完全的面向对象编程,而同时的Visual Basic等竞争对手只支持基于对象的编程(Object-Based Programming)。
- Object Pascal实现了在委托(Delegate)基础上的事件机制,完整地封装了Windows事件处理过程,这使得它对Windows GUI程序的开发提供了极好的支持。
- Object Pascal大幅度地强化了RTTI的能力,并在RTTI和PME(Property,Method,Event)模型的基础上实现了自己的开放性的Component机制,使得Delphi成为一个强大的组件化开发工具。
但是,Object Pascal的缺点也是相当明显的,这些缺点在长期的使用过程中逐一地暴露出来了。
- Object Pascal保留了全局变量/全局函数、Record等语法。和C++一样,这些特征提供了向后兼容性,保留了面向过程编程的风格,也许还获得了效率上的好处。但是,它们同样也破坏了面向对象的特性,很容易造成编程风格上的混乱(从理论上说,这一点不应引咎于编程语言,但这确实是常见的情况)。
- 和C++不一样,Object Pascal并不是一开始就是以大规模的程序开发为目标进行设计的。因此它一直不支持名字空间和嵌套类型等特性,而这些特性对于大规模的开发是相当重要的。
- Object Pascal的事件机制与Windows捆绑得过于紧密,当Object Pascal试图越过Windows GUI程序的开发,进入其它的领域时,开发者们或多或少地遇上了这些问题。
Java则是一种纯粹的面向对象编程的范例。“一切都是对象”是Java提出的并尽力予以贯彻的口号。和C++与Object Pascal不同,Java虽然在语法上尽力保持C的风格,但它并没有向后兼容的历史负担。Java在语法上也提供了对Interface等高级特性的支持。在这一切的基础之上,Java达到了无与伦比的明晰性,以及更浓厚的“OOP”的气息。和C++一样,Java一开始就被设计成一种承担大规模程序开发的语言,因此它对名字空间和嵌套类型的支持非常完善。另外,Java支持基于RTTI技术的反射(Reflection)机制,在这一点上已经超越了Object Pascal。因此,Java在组件化方面做得非常成功(JavaBean就是成果),这又导致了Java在分布式计算技术上的成功。在过去的几年中,随着网络的崛起,Java获得了空前的成功。
但是,Java并不是没有改进余地的。.NET/C#的出现有力地证明了这一点。
相比Java,C#最大的改进可能就是在效率方面。和Java一样,C#是一种纯粹的面向对象的编程语言,“一切都是对象”。对于Java中的一些优秀的语言特征,例如单根继承结构、Interface和Reflection等,C#也照单全收。但是,C#至少在以下几点上跟Java有着显著的不同:
- C#大幅度地提高了值类型(Value Types)的重要性。值类型是和引用类型(Reference Types)相对应而言的。在处理大对象的情况下,引用类型比值类型更有效率,对于小型的、频繁的对象创建,使用值类型远比引用类型更有效率。在Java中,除了基本类型以外,所有的变量都是引用类型。C#中的值类型包含基本类型和Struct,它们都派生自System.ValueType,可以统一地被作为对象处理。和Class的区别在于,Struct是一种值类型,在栈中创建,而且是隐式地封闭的(Sealed),因此比Class更有效率,可以作为一种轻量级的Class来使用。
- C#提供了一些和OOP没有直接关系,但在实际应用中非常有用的语法特征,例如对枚举(Enum)、委托(Delegate)、属性(Property)、预处理器(Preprocessor)和多播事件(Mutilcast Events)的支持。另外,C#还引入了一些C++支持而Java不支持的特性,如运算符重载。
- 在Java中,所有的类都是可以被继承的。C#提供了封闭类(Sealed Class)的功能。封闭类不能够被继承。封闭类可以使软件架构更为严谨,并且在运行期更有效率。
- C#提供了对不安全代码(unsafe code)的支持。在不安全代码中,C#提供了更加低层的能力,可以使用C风格的指针(Pointer),直接对内存进行操作。它的代价是失去了许多虚拟机上的特性,包括内存自动回收,类型安全,数组越界检查。
可以明显地感觉到,C#和Java二者在很多方面相似,但C#比Java更偏向实用性。无论是对值类型的重视,对属性、委托和预处理器的支持,还是对不安全代码的支持,都是跟OOP没有直接关系、甚至破坏OOP的“纯洁性”(例如预处理器)、但是在实际编码中又非常有用的特征。相比之下,Java显得更学院派,整体风格更为纯粹,或者说更为理想主义。对这二者的取舍,可能存在着不同的意见,但是无可置疑的是,在付出某些付价之后,C#确实在效率上和简洁性上超过了Java。
Delphi Language是比C#出现得更晚的语言,也是我们进行比较的语言中和C#最为相似的语言。由于CLS的规定,上述C#的特征几乎都可以适用于Delphi Language。但是,与C#不同的地方是,Delphi Language必须顾及与原来的Object Pascal的兼容性问题。
相对于Java和C#,Delphi Language的特征可以总结如下:
- Delphi Language保留了原有的.pas文件格式,也就是由Interface和Implementation组成的格式,这意味着类的声明和定义是分离的。这种风格可以追溯到C和C++。在Java和C#中,类的声明和定义不再分开了。
- Delphi Language保留了全局变量和全局函数。.NET是一个纯粹的面向对象的编程模型,C#也是纯粹的面向对象的语言,但是Delphi Language里面仍然保留了对全局变量和全局函数和支持,也就是仍然保持了面向过程编程的风格。
- Delphi Language支持MetaClass,这是Object Pascal语言中的类引用(Class Reference)的一种变形。类引用是Object Pascal独有的特征,Object Pascal通过类引用保存类的原始信息,并据此实现多态和RTTI等功能。.NET CLS中没有与类引用相对应的功能,因此Delphi Language引入了MetaClass的概念,通过编译器隐式地为每一个类加入一个嵌套类,在这个嵌套类中保存着类的原始信息。当需要类引用时,就从嵌套类中获取有关信息。
- Object Pascal,或者说Pascal的严谨和强类型的风格,在Delphi Language中也得到了体现。在C#中,Boxing/Unboxing可以通过直接在不同类型之间进行赋值来执行,但在Delphi Language中,必须通过更为严谨的方式,即显式造型来完成。
总结这些特征,我们可以得出一个大致的印象。由于必须符合.NET CLS的规范,Delphi Language进行了较大的扩展,支持许多高级的OO特性,从而使它在语言层次上已经达到了与Java和C#同样的水平。前文中已经说过,相比Java,C#更倾向于实用性,在这一点上,Delphi Language比C#有过之无不及。造成这一点的原因有两个,一个是因为Delphi Language不象C#那样是一种完全重新发明的语言,它必须照顾到向下的兼容性;另一个是因为Delphi Language,或者说Object Pascal,从一开始就是一种针对实际应用的语言,它的目标一直是如何方便而有效地满足用户需要,而不是符合某种特定的理论。
结论
C++的直接前驱是C语言,C是一种灵活性极大的语言,与硬件层关系紧密,因此被称为“可移植的汇编语言”,也被称为“中级语言”(与汇编这样的低级语言,以及Basic、Pascal这样的高级语言相对而言)。C提供了对汇编的一种抽象,使得不同平台之间的C语言的互相移植成为可能。同时,它的面向过程的特征,提供了模块化编程的能力,从而有利于大规模的软件开发工作。
C++将面向对象编程和泛型编程的思想引入了C语言,这些特征分别来自Smalltalk和Ada等语言。这些特征的加入,使得C++成为比C语言更高层次的抽象。这种抽象使得C++具有更高程度的移植性,也更适合开发大规模的软件。但同时,由于C++需要向下兼容C,使得它残留了许多C的不好的特征。C++的出现,可以视为C向更高层次的抽象的不那么彻底的尝试。
继C++之后,Object Pascal作为Pascal进行的同类尝试的产物出现了。Object Pascal针对C++的成功与失败之处进行了改进,改善了OO的机制,抛弃或简化了其中某些过于复杂或不贴近实用的设计,并在此基础之上建立了自己的IDE和组件机制。相比C++,Object Pascal并没有试图进行进一步的抽象,但是作为一种轻量级、实用性极强的语言,它在应用领域获得了极大的成功。
Java的诞生比Object Pascal略早,但是与Object Pascal相反,Java正是向更彻底的抽象努力的产物。Java通过虚拟机使它彻底地独立于任何平台,通过语法和API的强制贯彻了完整的OO思想,通过内存自动回收机制将原来由程序员完成的工作交给了虚拟机本身。Java彻底地改变了编程模型。由于Java是如此激进,因此它在诞生的前几年并没有得到普遍的接受,随后又由于同一原因获得了空前的成功,这可能是它的发明者自己也没有预料到的。
但是,历史并未就此终结。C#的出现,表示着对这一努力的某些过激之处的矫正。C#与Java在许多方面非常近似,但是,C#重新将实用性而不是理论放在第一位。对值类型的重视,对枚举、委托、属性、运算符重载、预处理器等特征的支持,都使得C#充满了更多的C++或Object Pascal的气味。这是从Java的抽象层次往后退了一步。
Delphi Language的特征与C#非常类似,如果说有区别的话,那就是Delphi Language更注重实用性,保留了许多与Object Pascal兼容的特性,因此位于更低的抽象层次上。如果将C/C++和Java看做两个极端,那么C#就位于这两个极端中间,而Delphi Language又位于C#和C/C++中间。如下图所示:
图二
我们也许可以作出如下结论:近年来计算机编程语言的演变,是一个从具体到抽象、再从抽象到具体有所回归的过程。编程语言本身就是人们企图对计算机硬件进行抽象的产物,随着软件的规模越来越大,编程语言也越来越复杂,抽象的层次越来越高。面向对象编程理论和泛型编程理论都是这种努力的成就,它们能够帮助编程语言达到更高的抽象层次。但是,从哲学的角度来说,人类的理性有其极限。试图用某种统一的方式对整个世界进行全面的解释的努力,固然是一种诱惑,但最后都不能避免失败的命运。抽象的层次越高,规范越严格,就会离硬件越远,牺牲越多的效率,最终不但远离了硬件,甚至也已经偏离了人类的自然思维方式,偏离了抽象原本要达到的目标。在Java达到了某种抽象的极限之后,C#和Delphi Language的出现是对这一理想化的倾向的反动。