3.1 语言的设计
接下来,我们从语言设计的角度,来比较一下 Java、JavaScript、Ruby 和 Go 这 4 种语言。这几种语言看起来彼此完全不同,但如果选择一个合适的标准,就可以将它们非常清楚地进行分类,如图 1 所示。
图 1 4 种语言的分类
Java 在最早的时候是作为客户端语言而诞生的。
JavaScript 是客户端语言的代表,Java 其实也在其黎明期作为客户端语言活跃过一段时间,应该有很多人还记得 Java Applet 这个名词。之后,Java 转型为服务器端语言的代表,地位也扶摇直上,但考虑到它的出身,这里还是将其分类为客户端语言。
另一个分类标准,就是静态和动态。所谓静态,就是不实际运行程序,仅通过程序代码的字面来确定结果的意思;而所谓动态,就是只有当运行时才确定结果的意思。静态、动态具体所指的内容有很多种,大体上来分的话就是运行模式和类型。这 4 种语言全都具备面向对象的性质,而面向对象本身就是一种包含动态概念的性质。不过,在这几种语言之中,Java 和 Go 是比较偏重静态一侧的语言,而 Ruby 和 JavaScript 则是比较偏重动态一侧的语言。
客户端与服务器端
首先,我们先将这些语言按照客户端和服务器端来进行分类。如前面所说,这种分类是以该语言刚刚出现时所使用的方式为基准的。
现在 Java 更多地被用作服务器端语言,而我们却将它分类到客户端语言中,很多人可能感到有点莫名其妙。Java 确实现在已经很少被用作客户端语言了,但是我们不能忘记,诞生于 1995 年的 Java,正是伴随嵌入在浏览器中的 Applet 技术而出现的。
Java 将虚拟机(VM)作为插件集成到浏览器中,将编译后的 Java 程序(Applet)在虚拟机上运行,这种技术当初是为了增强浏览器的功能。再往前追溯的话,Java 原本名叫 Oak,是作为面向嵌入式设备的编程语言而诞生的。因此,从出身来看的话,Java 还是一种面向客户端的编程语言。
Java 所具备的 VM 和平台无关性字节码等特性,本来就是以在客户端运行 Applet 为目的的。在各种不同的环境下都能够产生相同的行为,这样的特性对于服务器端来说虽然也不能说是毫无价值,但是服务器环境是可以由服务提供者来自由支配的,因此至少可以说,这样的特性无法带来关键性的好处吧。另一方面,在客户端环境中,操作系统和浏览器都是千差万别,因此对平台无关性的要求一直很高。
Java 诞生于互联网的黎明时期,那个时候浏览器还不是电脑上必备的软件。当时主流的浏览器有 Mosaic 和 Netscape Navigator 等,1 除此之外还有一些其他类似的软件,而 Internet Explorer 也是刚刚才崭露头角。
1 Mosaic 是世界上第一款真正流行的互联网浏览器软件,由美国国家超级计算机应用中心(National Center for Supercomputing Applications,NCSA)开发,1993 年发布,1997 年停止开发。Netscape Navigator 是由网景公司(Netscape)开发的一款互联网浏览器软件,1994 年发布,曾经一度是市场占有率最高的浏览器软件,后来它的地位被微软的 Internet Explorer 所取代。
在那个充满梦想的时代,如果能开发出一种功能上有亮点的浏览器就有可能称霸业界。原 Sun Microsystems 公司2曾推出了一个用 Java 编写的浏览器 HotJava,向世界展示了 Applet 的可能性。然而,随着浏览器市场格局的逐步固定,他们转变了策略,改为向主流浏览器提供插件来集成 Java,从而对 Applet 的运行提供支持。
2 Sun Microsystems 于 2010 年以 74 亿美元被 Oracle 收购。
向服务器端华丽转身
然而,Java 自诞生之后,并未在客户端方面取得多大的成功,于是便开始着手进入服务器端领域。造成这种局面有很多原因,我认为其中最主要的原因应该是在 Applet 这个平台上迟迟没有出现一款杀手级应用(killer app)。
处于刚刚诞生之际的 Java 遭到了很多批判,如体积臃肿、运行缓慢等,不同浏览器上的 Java 插件之间也存在一些兼容性方面的问题,使得 Applet 应用并没有真正流行起来。在这个过程中,JavaScript 作为客户端编程语言则更加实用,并获得了越来越多的关注。当然,在那个时候 Java 已经完全确立了自己作为服务器端编程语言的地位,因此丧失客户端这块领地也不至于感到特别肉痛。
Java 从客户端向服务器端的转身可以说是相当成功的。与此同时,Sun Microsystems 和 IBM 等公司着手对 JVM(Java VM)进行改良,使得其性能得到了改善,在某些情况下性能甚至超越了 C++。想想之前对 Java 性能恶评如潮的情形,现在 Java 能有这样的性能和人气简直就像做梦一样。
在服务器端获得成功的四大理由
由于我本人没有大规模实践过 Java 编程,因此对于 Java 在服务器端取得成功的来龙去脉,说真的并不是很了解。不过,如果让我想象一下的话,大概有下面几个主要的因素。
1. 可移植性
虽然服务器环境比客户端环境更加可控,但服务器环境中所使用的系统平台种类也相当多, 如 Linux、Solaris、FreeBSD、Windows 等,根据需要,可能还会在系统上线之后更换系统平台。在这样的情况下,Java 所具备的“一次编写,到处运行”特性就显得魅力十足了。
2. 功能强大
Java 在服务器端崭露头角是在 20 世纪 90 年代末,那个时候的状况对 Java 比较有利。和 Java 在定位上比较相似的语言,即静态类型、编译型、面向对象的编程语言,属于主流的也就只有 C++ 而已了。
在 Java 诞生的 20 世纪 90 年代中期,正好是我作为 C++ 程序员开发 CAD 相关系统的时候。但当时 C++ 也还处于发展过程中,在实际的开发中,模板、异常等功能还无法真正得到运用。
相比之下,Java 从一开始就具备了垃圾回收(GC)机制,并在语言中内置了异常处理,其标准库也是完全运用了异常处理来设计的,这对程序员来说简直是天堂。毫无疑问,Java 语言的这些优秀特性,是帮助其确立服务器端编程语言地位的功臣之一。
3. 高性能
Java 为了实现其“一次编写,到处运行”的宣传口号,并不是将程序直接转换为系统平台所对应的机器语言,而是转换为虚拟 CPU 的机器语言“字节码”(Bytecode),并通过搭载虚拟 CPU 的模拟器 JVM 来运行。JVM 归根到底其实是在运行时用来解释字节码的解释器,理论上说运行速度应该无法与直接生成机器语言的原生编译器相媲美。
事实上,在 Java 诞生初期,确实没有达到编译型语言应有的运行速度,当时的用户经常抱怨 Java 太慢了,这样的恶评令人印象深刻。
然而,技术的革新是伟大的。随着各种技术的进步,现在 Java 的性能已经能够堪称顶级。
例如,有一种叫做 JIT(Just In Time)编译的技术,可以在运行时将字节码转换成机器语言,经过转换之后就可以获得和原生编译一样快的运行速度。在运行时进行编译,就意味着编译时间也会包含在运行时间里面。因此,优秀的 JIT 编译器会通过侦测运行信息,仅将需要频繁运行的瓶颈部分进行编译,从而大大削减编译所需的时间。而且,利用运行时编译,可以不用考虑连接的问题而积极运用内联扩展3,因此在某些情况下,运行速度甚至可以超过 C++。
3 内联扩展(Inline expansion)是指让编译器直接将完整的函数体插入到每一个调用该函数的地方,从而提高函数调用的运行速度。
在 Java 中,其性能提高的另一个障碍就是 GC。GC 需要对对象进行扫描,将不用的对象进行回收,这个过程和程序本身要进行的操作是无关的,换句话说,就是做无用功,因此而消耗的时间拖累了 Java 程序的性能。作为对策,在最新的 JVM 中,采用了并行回收、分代回收4等技术。
4 并行回收可以将 GC 放在单独的线程中运行,从而对程序本身的处理(基本上)不造成影响。分代回收是在扫描过程中忽略程序运行中一直存活的长寿对象,从而减少扫描工作量,降低 GC 开销的技术。(原书注)
4. 丰富的库
随着 Java 的人气直升,应用逐渐广泛,Java 能够使用的库也越来越多。库的增加提高了开发效率,从而又反过来拉高了 Java 的人气,形成了一个良性循环。现在 Java 的人气已经无可撼动了。
客户端的 JavaScript
Applet 在客户端对扩展浏览器功能做出了尝试,然而它并不太成功。在浏览器画面中的一个矩形区域中运行应用程序的 Applet,并没有作为应用程序的发布手段而流行起来。
几乎是在同一时期出现的 JavaScript,也是一种集成在浏览器中的语言,但是它可以在一般的网页中嵌入程序逻辑,这一点是和 Java Applet 完全不同的方式,却最终获得了成功。
JavaScript 是由原 Netscape Communications 公司5 开发的,通过 JavaScript,用户点击网页上的链接和按钮时,不光可以进行页面的跳转,还可以改写页面的内容。这样的功能十分便利,因此 Netscape Navigator 之外的很多浏览器都集成了 JavaScript。
5 网景通信(Netscape Communications)于 1998 年被美国在线(AOL)收购,并于 2003 年解散。
随着浏览器的不断竞争和淘汰,当主流浏览器全部支持 JavaScript 时,情况便发生了变化。像 Google 地图这样的产品,整体的框架是由 HTML 组成的,但实际显示的部分却是通过 JavaScript 来从服务器获取数据并显示出来,这样的手法从此开始流行起来。
在 JavaScript 中与服务器进行异步通信的 API 叫做 XMLHttpRequest,因此从它所衍生出的手法便被称为 Ajax(Asynchronous JavaScript and XML,异步 JavaScript 与 XML)。在美国有一种叫做 Ajax 的厨房清洁剂,说不定是从那个名字模仿而来的。
性能显著提升
目前,客户端编程语言中 JavaScript 已成为一个强有力的竞争者,伴随着 JavaScript 重要性的不断提高,对 JavaScript 引擎的投资也不断增加,使 JavaScript 的性能得到了显著改善。改善 JavaScript 性能的主要技术,除了和 Java 相同的 JIT 和 GC 之外,还有特殊化(Specialization)技术。
与 Java 不同,JavaScript 是一种动态语言,不带有变量和表达式的类型信息,针对类型进行优化是非常困难的,因此性能和静态语言相比有着先天的劣势,而特殊化就是提高动态语言性能的技术之一。
我们设想图 2 所示的这样一个 JavaScript 函数。这个函数是用于阶乘计算的,大多数情况下,其参数 n 应该都是整数。由于 JIT 需要统计运行时信息,因此 JavaScript 解释器也知道参数 n 大多数情况下是整数。
function fact(n) { if (n == 1) return 1; return n * fact(n-1); }图 2 JavaScript 函数
于是,当解释器对 fact 函数进行 JIT 编译时,会生成两个版本的函数:一个是 n 为任意对象的通用版本,另一个是假设 n 为整数的高速版本。当参数 n 为整数时(即大多数情况下),就会运行那个高速版本的函数,便实现了与静态语言几乎相同的运行性能。
除此之外,最新的 JavaScript 引擎中还进行了其他大量的优化6,说 JavaScript 是目前最快的动态语言应该并不为过。
6 这些优化包括将 JavaScript 中本来以散列表(Hash table)形式实现的对象进行数组化,从而提高访问速度;通过内联化和特殊化相结合,实现和静态语言同等的对象访问速度等技术。(原书注)
JavaScript 在客户端称霸之后,又开始准备向服务器端进军了7。JavaScript 的存在感在将来应该会越来越强吧。
7 在各种服务器端 JavaScript 的尝试中,最有力的一种就是 node.js。node.js 是将 Google Chrome 中搭载的高速 JavaScript 引擎 v8,与异步 I/O 相结合的产物。在第 6 章中我们将介绍 node.js。(原书注)
服务器端的 Ruby
客户端编程的最大问题,就是必须要求每一台客户端都安装相应的软件环境。在 Java 和 JavaScript 诞生的 20 世纪 90 年代后半,互联网用户还只局限于一部分先进的用户,然而现在互联网已经大大普及,用户的水平构成也跟着变得复杂起来,让每一台客户端都安装相应的软件环境,就会大大提高软件部署的门槛。
而相对的,在服务器端就没有这样的制约,可以选择最适合自己的编程语言。
在 Ruby 诞生的 1993 年,互联网还没有现在这样普及,因此 Ruby 也不是一开始就面向 Web 服务器端来设计的。然而,从 WWW 黎明期开始,为了实现动态页面而出现了通用网关接口(Common Gateway Interface,CGI)技术,而 Ruby 则逐渐在这种技术中得到了应用。
所谓 CGI,是通过 Web 服务器的标准输入输出与程序进行交互,从而生成动态 HTML 页面的接口。只要可以对标准输入输出进行操作,那么无论任何语言都可以编写 CGI 程序,这不得不归功于 WWW 设计的灵活性,使得动态页面可以很容易地编写出来,也正是因为如此,使得 WWW 逐渐风靡全世界。
在 WWW 中,来自 Web 服务器的请求信息是以文本的方式传递的,反过来,返回给 Web 服务器的响应信息也是以文本(HTML)方式传递的,因此擅长文本处理的编程语言就具有得天独厚的优势。于是,脚本语言的时代到来了。以往只是用于文本处理的脚本语言,其应用范围便一下子扩大了。
早期应用 CGI 的 Web 页面大多是用 Perl 来编写的,而作为“Better Perl”的 Ruby 也随之逐步得到越来越多的应用。
Ruby on Rails 带来的飞跃
2004 年,随着 Ruby on Rails 的出现,使得 Web 应用程序的开发效率大幅提升,也引发了广泛的关注。当时,已经出现了很多 Web 应用程序框架,而 Ruby on Rails 可以说是后发制人的。Ruby on Rails 的特性包括:
· 完全的 MVC 架构
· 不使用配置文件(尤其是 XML)
· 坚持简洁的表达
· 积极运用元编程
· 对 Ruby 核心的大胆扩展
基于这些特性,Ruby on Rails 实现了很高的开发效率和灵活性,得到了广泛的应用。可以说,Ruby 能拥有现在的人气,基本上都是 Ruby on Rails 所作出的贡献。
目前,作为服务器端编程语言,Ruby 的人气可谓无可撼动。有一种说法称,以硅谷为中心的 Web 系创业公司中,超过一半都采用了 Ruby。
但这也并不是说,只要是服务器端环境,Ruby 就一定可以所向披靡。在规模较大的企业中,向网站运营部门管理的服务器群安装软件也并不容易。实际上,在某个大企业中,曾经用 Ruby on Rails 开发了一个面向技术人员的 SNS,只用很短的时间就完成搭建了,但是等到要正式上线的时候,运营部门就会以“这种不知道哪个的家伙开发的,也没经过第三方安全认证的 Ruby 解释器之类的软件,不可以安装在我们数据中心的主机上面”这样的理由来拒绝安装,这真是相当头疼。
不过,开发部门的工程师们并没有气馁,而是用 Java 编写的 Ruby 解释器 JRuby,将开发好的 SNS 转换为 jar 文件,从而使其可以在原 Sun Microsystems 公司的应用程序服务器 GlassFish 上运行。当然,JVM 和 GlassFish 都已经在服务器上安装好了,这样一来运营方面也就没有理由拒绝了。多亏了 JRuby,结局皆大欢喜。
JRuby 还真是在关键时刻大显身手呢。
服务器端的 Go
Go 是一种新兴的编程语言,但它出身名门,是由著名 UNIX 开发者罗勃·派克和肯·汤普逊8开发的,因此受到了广泛的关注。
8 罗勃 • 派克(Rob Pike,1956— )是加拿大程序设计师,早年为贝尔实验室的 UNIX 小组成员,曾参与设计贝尔实验室 9 号计划(Plan 9)、Inferno 操作系统和 Limbo 编程语言,目前就职于 Google 公司。肯 • 汤普逊(Ken Thompson,1943— )是美国计算机科学家,曾参与设计 Plan 9、B 语言、C 语言,并于 1983 年获得图灵奖。
Go 的诞生背景源于 Google 公司中关于编程语言的一些问题。在 Google 公司中,作为优化编程环境的一环,在公司产品开发中所使用的编程语言,仅限于 C/C++、Java、Python 和 JavaScript。实际上也有人私底下在用 Ruby,不过正式产品中所使用的语言仅限上述 4 种。9
9 此外还有一些内部专用的语言,例如,为了让用于处理 Web 爬虫所抓取的大量数据的 MapReduce 运行更高效,使用了专用语言 Sawzall。(原书注)
这 4 种语言在使用上遵循着一定的分工:客户端语言用 JavaScript,服务器端语言用脚本系的 Python,追求大规模或高性能时用 Java,文件系统等面向平台的系统编程用 C/C++。在这些语言中,Google 公司最不满意的就是 C/C++ 了。
和其他一些编程语言相比,C/C++ 的历史比较久,因此不具备像垃圾回收等最近的语言所提供的编程辅助功能。因此,由于开发效率一直无法得到提高,便产生了设计一种“更好的”系统编程语言的需求。而能够胜任这一位置的,正是全新设计的编程语言 Go。
Go 具有很多特性,(从我的观点来看)比较重要的有下列几点:
· 垃圾回收
· 支持并行处理的 Goroutine 10
10 Goroutine 是 Go 特有的一个术语,简单来说就是轻量版的线程。随着多核处理器的普及,并发编程的重要性不断提高,然而 C/C++ 在语言层面并不支持并发编程。在内核层面可以使用线程(pthread 等),但用起来并没有那么方便。在 Go 中,通过在语言层面所提供的支持,就很好地支持了系统编程层面中对并发编程的有效利用。(原书注)
· Structural Subtyping(结构子类型)
关于最后一点 Structural Subtyping,我们会在后面对类型系统的讲解中进行说明。
静态与动态
刚才我们已经将这 4 种语言,从客户端、服务器端的角度进行了分类。接下来我们再从动态、静态的角度来看一看这几种语言。
正如刚才所讲过的,所谓静态,就是无需实际运行,仅根据程序代码就能确定结果的意思;而所谓动态,则是只有到了运行时才能确定结果的意思。
不过,无论任何程序,或多或少都包含了动态的特性。如果一个程序完全是静态的话,那就意味着只需要对代码进行字面上的分析,就可以得到所有的结果,这样一来程序的运行就没有任何意义了。例如,编程计算 6 的阶乘,如果按照完全静态的方式来编写的话,应该是下面这样的:
puts "720"不过,除非是个玩具一样的演示程序,否则不会开发出这样的程序来。在实际中,由于有了输入的数据,或者和用户之间的交互,程序才能在每次运行时都能得到不同的要素。
因此,作为程序的实现者,编程语言也多多少少都具备动态的性质。所谓动态还是静态,指的是这种语言对于动态的功能进行了多少限制,或者反过来说,对动态功能进行了多少积极的强化,我们所探讨的其实是语言的这种设计方针。
例如,在这里所列举的 4 种编程语言都是面向对象的语言,而面向对象的语言都会具备被称为多态(Polymorphism)或者动态绑定的动态性质。即,根据存放在变量中的对象的实际性质,自动选择一种合适的处理方式(方法)。这样的功能可以说是面向对象编程的本质。
属于动态的编程语言,其动态的部分,主要是指运行模式和类型。这两者是相互独立的概念,但采用动态类型的语言,其运行模式也具有动态的倾向;反之也是一样,在静态语言中,运行模式在运行时的灵活性也会受到一定的限制。
动态运行模式
所谓动态运行模式,简单来说,就是运行中的程序能够识别自身,并对自身进行操作。对程序自身进行操作的编程,也被称为元编程11(Metaprogramming)。
11 程序“对自身进行操作”也被称为“反射”(Reflection)。在英语中,Reflection 一词有“对自己进行反省”的意思。同样的意思,在 Ruby 大多被称为元编程,而在 Java 中则大多被称为反射。(原书注)
在 Ruby 和 JavaScript 中,元编程是十分自然的,比如查询某个对象拥有哪些方法,或者在运行时对类和方法进行定义等等,这些都是理所当然的事。
另一方面,在 Java 中,类似元编程的手法,是通过“反射 API”来实现的。虽然对类进行取出、操作等功能都是可以做到的,但并非像 Ruby 和 JavaScript 那样让人感到自由自在,而是“虽然能做到,但一般也不会去用”这样的感觉吧。
Go 也是一样。在 Go 中,通过利用 reflect 包可以获取程序的运行时信息(主要是类型),但是(在我所理解的范围内)无法实现进一步的元编程功能。而之所以没有采用比 Java 更进一步的动态运行模式,恐怕是因为这(可能)在系统编程领域中必要性不大,或者是担心对运行速度产生负面影响之类的原因吧。
何谓类型
从一般性的层面来看,类型12指的是对某个数据所具有的性质所进行的描述。例如,它的结构是怎样的,它可以进行哪些操作,等等。动态类型的立场是数据拥有类型,且只有数据才拥有类型;而静态类型的立场是数据拥有类型,而存放数据的变量、表达式也拥有类型,且类型是在编译时就固定的。
12 其实,类型这个话题,在现在许多计算机科学论文所设计的领域中,算是比较热门的。不过在这里,我们不去探讨那些最尖端的话题。说实话,我的数学素养实在不行,对于那些通篇都是数学公式的类型理论方面的论文,实在是无法理解,因此很遗憾,我也就没办法讲解这些内容了。(原书注)
然而,即便是静态类型,由于面向对象语言中的多态特性,也必须具备动态的性质,因此需要再追加一条规则,即实际的数据(类型),是静态指定的类型的子类型。所谓子类型(Subtype),是指具有继承关系,或者拥有同一接口,即静态类型与数据的类型在系统上“拥有同一性质”。
静态类型的优点
动态类型比较简洁,且灵活性高,但静态类型也有它的优点。由于在编译时就已经确定了类型,因此比较容易发现 bug。当然,程序中的 bug 大多数都是与逻辑有关的,而单纯是类型错误而导致的 bug 只是少数派。不过,逻辑上的错误通常也伴随着编译时可以检测到的类型不匹配,也就是说,通过类型错误可以让其他的 bug 显露出来。
除此之外,程序中对类型的描述,可以帮助对程序的阅读和理解,或者可以成为关于程序行为的参考文档,这可以说是一个很大的优点。
此外,通过静态类型,可以在编译时获得更多可以利用的调优信息,编译器便可以生成更优质的代码,从而提高程序的性能。然而,通过 JIT 等技术,动态语言也可以获得与原生编译的语言相近的性能,这也说明,在今后静态语言和动态语言之间的性能差距会继续缩小。
动态类型的优点
相对而言,动态类型的优点,就在于其简洁性和灵活性了。
说得极端一点的话,类型信息其实和程序运行的本质是无关的。就拿阶乘计算的程序来说,无论是用显式声明类型的 Java 来编写(图 3),还是用非显式声明类型的 Ruby 来编写(图 4), 其算法都是毫无区别的。然而,由于多了关于类型的描述,因此在 Java 版中,与算法本质无关的代码的分量也就增加了。
class Sample { private static int fact(int n) { if (n == 1) return 1; return n * fact(n - 1); } public static void main(String[] argv) { System.out.println("6!="+fact(6)); } }图 3 Java 编写的阶乘程序
def fact(n) if n == 1 1 else n * fact(n - 1) end end print "6!=", fact(6), "\n" ---图 4 Ruby 编写的阶乘程序
而且,类型也带来了更多的制约。图 3、图 4 中所示的程序对 6 的阶乘进行了计算,但如果这个数字继续增大,Java 版对超过 13 的数求阶乘的话,就无法正确运行了。图 3 的程序中,fact 方法所接受的参数类型显式声明为 int 型,而 Java 的 int 为 32 位,即可以表示到接近 20 亿的整数。如果阶乘的计算结果超出这个范围13,就会导致溢出。
13 13 的阶乘结果为 6227020800,而 Java 中 int 型的表示范围为 -2147483648 到 2147483647。
当然,由于 Java 拥有丰富的库资源,用 BigInteger 类就可以实现无上限的大整数计算,但这就需要对上面的程序做较大幅度的改动。而由于计算机存在“int 的幅度为 32 位”这一限制,就使得阶乘计算的灵活性大大降低了。
另一方面,Ruby 版中则没有这样的制约,就算是计算 13 的阶乘,甚至是 200 的阶乘,都可以直接计算出来,而无需担心如 int 的大小、计算机的限制等问题。
其实这里还是有点小把戏的。同样是动态语言,用图 1 中的 JavaScript 来计算 200 的阶乘就会输出 Infinity(无穷大)。其实,JavaScript 的数值是浮点数,因此无法像 Ruby 那样支持大整数的计算。也就是说,要不受制约地进行计算,除了类型的性质之外,库的支持也是非常重要的。
有鸭子样的就是鸭子
在动态语言中,一种叫做鸭子类型(Duck Typing)的风格被广泛应用。鸭子类型这个称谓,据说是从下面这则英语童谣来的:
If it walks like a duck and quacks like a duck, it must be a duck.(如果像鸭子一样走路,像鸭子一样呱呱叫,则它一定是一只鸭子)
从这则童谣中,我们可以推导出一个规则,即如果某个对象的行为和鸭子一模一样,那无论它真正的实体是什么,我们都可以将它看做是一只鸭子。也就是说,不考虑某个对象到底是哪一个类的实例,而只关心其拥有怎样的行为(拥有哪些方法),这就是鸭子类型。因此,在程序中,必须排除由对象的类所产生的分支。
这是由“编程达人”大卫·托马斯(Dave Thomas)所提出的。
例如,假设存在 log_puts(out, mesg) 这样一个方法,用来将 mesg 这个字符串输出至 out 这个输出目标中。out 需要指定一个类似 Ruby 中的 IO 对象,或者是 Java 中的 OutPutStream 这样的对象。在这里,本来是向文件输出的日志,忽然想输出到内存的话,要怎么办呢?比如说我想将日志的输出结果合并成一个字符串,然后再将它取出。
在 Java 等静态语言中,out 所指定的对象必须拥有共同的超类或者接口,而无法选择一个完全无关的对象作为输出目标。要实现这样的操作,要么一开始就事先准备这样一个接口,要么重写原来的类,要么准备一个可以切换输出目标的包装对象(wrapper object)。无论如何,如果没有事先预计到需要输出到内存的话,就需要对程序进行大幅的改动了。
如果是采用了鸭子类型风格的动态语言,就不容易产生这样的问题。只要准备一个和 IO 对象具有同样行为的对象,并将其指定为 out 的话,即便不对程序进行改动,log_puts 方法能够成功执行的可能性也相当大。实际上,在 Ruby 中,确实存在和 IO 类毫无继承关系,但和 IO 具有同样行为的 StringIO 类,用来将输出结果合并成字符串。
动态类型在编译时所执行的检查较少,这是一个缺点,但与此同时,程序会变得更加简洁,对于将来的扩展也具有更大的灵活性,这便是它的优点。
Structural Subtyping
在 4 种语言中最年轻的 Go,虽然是一种静态语言,但却吸取了鸭子类型的优点。Go 中没有所谓的继承关系,而某个类型可以具有和其他类型之间的可代换性,也就是说,某个类型的变量中是否可以赋予另一种类型的数据,是由两个类型是否拥有共同的方法所决定的。例如,对于“A 型”的变量,只要数据拥有 A 型所提供的所有方法,那么这个数据就可以赋值给该变量。像这样,以类型的结构来确定可代换性的类型关系,被称为结构子类型(Structural Subtyping);另一方面,像 Java 这样根据声明拥有继承关系的类型具有可代换性的类型关系,被称为名义子类型(Nominal Subtyping)。
在结构子类型中,类型的声明是必要的,但由于并不需要根据事先的声明来确定类型之间的关系,因此就可以实现鸭子类型风格的编程。和完全动态类型的语言相比,虽然增加了对类型的描述,但却可以同时获得鸭子类型带来的灵活性,以及静态编译所带来了类型检查这两个优点,可以说是一个相当划算的交换。
小结
在这里,我们对 Ruby、JavaScript、Java、Go 这 4 种语言,从服务器端、客户端,以及静态、动态这两个角度来进行了对比。这 4 种语言由于其不同的设计方针,而产生出了不同的设计风格,大家是否对此有了些许了解呢?
不仅仅是语言,其实很多设计都是权衡的结果。新需求、新环境,以及新范式,都催生出新的设计。而学习现有语言的设计及其权衡的过程,也可以为未来的新语言打下基础。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论