纵论流行的编程语言【转】
自计算机诞生到现在,出现了难以计数的程序语言。排除了那些没有广泛使用的语言,我们还是有很多选择来完成我们的任务。虽然很多语言都能完成多方面的工作,但是,每种语言适合的领域还是有一些限制的。从初一开始写程序到现在,接触过好些语言了,它们主要有几个层次
- 中低级语言:ASM, C, Pascal。 这些语言能够在很大程度上对硬件和内存进行操作,效率很高。很多朋友现在一直视C/Pascal的指针为编程的最爱,通过它似乎你能够控制几乎所有的事 情。但是,这种语言开发周期长,而且由于限制少,危险性也相应比较高,一旦发现错误(尤其是由于指针和内存操作不小心引起的诡异问题),追踪难度很大。
- 具有面向对象特性的高级语言:C++, C#, Java。 这类语言是商业应用的主流开发工具。它们具有的对象封装,多态,泛型,以及异常处理机制基本能满足很多现实需求。从C++的RTTI开始,这些语言已经逐 步建立反射(Reflection)的能力。到了C#和Java,反射已经很成熟,并且在它们的新标准中,Annotation(Meta-data)也 开始引入了。
- 动态脚本语言:Python/Ruby。 它们使程序的运行时灵活性到达了新的高度,而且开发周期也大为缩短。我最近完成的一些事情,使我对此很有感触,一些在普通高级语言需要很多代码才能完成的 事情,可以通过随手写就的精巧脚本就轻松完成了。这些语言具有了全方位的反射和动态表换的能力,甚至可以在运行时动态构建出新的类型。我们经常用的MATLAB也是属于此类,只是它的使用领域相对较窄。
目前,也常切换使用几种语言来共同完成某项工作:C++用于撰写效率关键的代码段,C#/Java用于构造带有完整GUI的软件系统,Ruby用于撰写脚本完成一些诸如批量文件处理,数据收集组织的任务。
这些语言有很多类似的地方,却有着有趣的区别:
- 泛型(Generic Programming)。通 俗的说就是带参数定义的类型,大家很熟悉的例子可能就是C++里面的std::vector<T>。在一般印象中,它就是用来定义一个容器中 的元素类型的。这固然是它的功能之一,但是这远不能反映泛型编程的全部能力。其实在很多系统中,它更多地用在诸如Policy, Dependency Injection等的设计模式当中,实施类型和策略之间的某种绑定。在C++, C# (ver 2.0 or above), Java (SE 5.0 or above) 都实现了泛型,但是它们三种语言就有三种本质上完全不同的实现方式。
- C++采用 Template技术,在编译器依据不同的Type parameter产生多个独立的类型定义。它的好处在于,泛型相关操作都在编译期完成,因此很多情况下不需要以牺牲运行时效率为代价,效率高。但是,由 于它为同一模板的所有不同参数都各自产生新的实际类型,在一些复杂系统中可能造成代码体积的急剧膨胀,从而导致编译缓慢和影响运行效率。另外,C++在生 成实际类型之前不会对模板进行检测,而且生成过程带有选择性,因此使得Template的运用可以非常灵活,并由此引发了一种独特的编程方式:静态元编程 (static meta-programming),就是通过模板技术的灵活运用,在编译期就实现很多重要的能力。C++开源社区里面著名的Boost和Loki库就是 这方面的代表。 这种灵活性的一个代价,就是让编译错误变得难以定位。有时候,模板声明中的一些小错误由于经过多次实例生成和类型推断过程的扩散,可以衍 生出数以百计的莫名其妙的编译问题。
- C# 2.0采用的是参数化的类型,就是每个泛型类型只有一处类型定义,在它的对象的类信息中带有类型参数的信息。这种技术是一种比较完善的实现,在灵活性和可控性之间达到了良好的平衡。借助C#强有力的反射机制,可以以较小的运行时代价安全地实施很多元编程的技术。
- Java 采用的是Type Erasure。可以允许在源码中声明类参数,但是它们只对编译器有效。编译器通过它们进行类型安全检查,并且在bytecode里面适当地方插入类型转 换指令,之后在生成的bytecode中就不再存在类参数信息了。一方面,它采用这样的技术保持了泛化容器和旧版代码中非泛型容器的兼容,但是另外一方 面,却非常可惜地放弃了在运行期对类型参数进行反射的能力。也就是说,你将没法在运行期判断一个列表究竟是 ArrayList<String>还是ArrayList<Person>了。另外,Java的类型参数不能是原始类型,也就 是说不能做出诸如ArrayList<double>这类容器。如果非要让一个容器装double,就必须先对这些数字进行装箱 (boxing),变成Double,这样就往往造成了运行时效率的大为下降了。
动态语言一般没有这种东西,因为它们不需要。它们对于类型具有强大的动态定制能力,不需要通过泛型技术进行静态定制。在某种意义上,泛型是对于普通高级语言缺乏动态类型定义能力的一种补偿。
- 反射(Reflection)。就 是在运行时获取类型信息的能力。在最传统的语言中,比如C,对类型的运行时感知能力很有限,sizeof(t)算是比较重要的一个了,基本上不能做出 typeof(t)之类的东西——当然,你可以考虑自己实现一个运行期类型表——但是这已经不是语言的事情了。C++到了1997年标准的时候才正式规定 了RTTI(Runtime Type Identitifcation,运行期类型识别),但是所支持功能非常有限,主要是做对诸如dynamic_cast的帮助。 Microsoft的 MFC里面也自己搞了一套类似的东西,主要是大量依赖预编译宏来实现的,用现在的程序设计观点来看,实在是相当拙劣。
到 Java/C#,Reflection才正式作为一种重要的语言特性来实现,这得益于以Object为root的单根继承体系。它们都有一个专用类 Class/Type来表达类型信息,并且提供了对类型构造进行各种检索的能力,比如查询一个类含有多少公用方法,它的父类是什么,实现了什么接口之类。
- 函数代理和回调(Delegate/Callback)。函 数或者执行代码作为参数传递是实现灵活的执行控制的重要手段。从C开始,就已经可以实现。在C/C++里面,一种惯用的方法就是函数指针。但是,由于它能 够和void *进行任意转换,因此并不具有类型安全的特性。而且这个指针可以在内存和代码段中任意移动,可能造成很大的安全隐患。在C++里面,还有一种新的类型安全 的实施方法,就是仿函数(functor),它主要通过重载括号运算符,把class模拟成为function来使用。这种手法在STL被广泛运用。相比 于函数指针,functor往往通过不带成员数据的light weight class来实现,函数执行可能在编译期便进行绑定,有时效率更高。而且与其它泛型技术比如binding等的结合十分灵活。
在C#里面,在1.0标准就在语言层次内建了delegate类型,并且提供了丰富的操作支持。到了2.0,更进一步支持泛型的delegate。而且,在delegate基础上实现了event。可以说,对回调的支持已经相当完备。
Java 并不直接支持函数作为对象来传递。类似的功能通常是通过interface来间接完成的。可以通过定义单方法的接口来模拟一个代理。相对来说,这种做法比 起C#显得繁琐。好在Java支持局部的匿名类(Anonymous class)定义,可以在一定程度上弥补方便性的不足。基于接口定义的函数传递同样是类型安全的。
其实,代码传递的最方便的写法是 Lambda表达式,直接简洁地表达一个输入输出关系。在Ruby和Matlab都已经支持这种写法。即将推出的C# 3.0也将准备引入这个特性——这是C# 3.0令人期待的一个重要特性。而Java下一个版本SE 7.0,也可能考虑加入类似的东西,叫做closure。可见,便捷和可靠的运行期代码选择和传递将成为编程语言发展的趋势。