专题:码农论剑

专题:码农论剑

梦寐以求的编程语言

{%}

作者/ Paul Graham

Lisp专家,世界上首个互联网应用程序Viaweb开发人之一。创建的Viaweb公司后被雅虎收购,改名为Yahoo!Store。2005年创办Y Combinator,开创了天使投资新模式,被《福布斯》杂志喻为“撼动硅谷的人”。目前为止其公司扶持的创业公司已有250余家,成功的超过80%。Graham是当之无愧的“硅谷创业之父”。其个人网站是http://www.paulgraham.com

 

一心让臣民行善的暴君可能是最专制的暴君。

——C. S. LEWIS(1898—1963,英国小说家)

我的朋友曾对一位著名的操作系统专家说他想要设计一种真正优秀的编程语言。那位专家回答,这是浪费时间,优秀的语言不一定会被市场接受,很可能无人使用,因为语言的流行不取决于它本身。至少,那位专家设计的语言就遭遇到了这种情况。

那么,语言的流行到底取决于什么因素呢?流行的语言是否真的值得流行呢?还有必要尝试设计一种更好的语言吗?如果有必要的话,怎样才能做到这一点呢?

为了找到这些问题的答案,我想我们可以观察黑客,了解他们使用什么语言。编程语言本来就是为了满足黑客的需要而产生的,当且仅当黑客喜欢一种语言时,这种语言才能成为合格的编程语言,而不是被当作“指称语义”(denotational semantics)或者编译器设计。

流行的秘诀

没错,大多数人选择某一种编程语言,不是因为这种语言有什么独特的特点,而是因为听说其他人使用这种语言。但是我认为,外界因素对于编程语言的流行其实没有想象中那么大的影响力。我倒是觉得,问题出在对于什么是优秀编程语言,黑客的看法与大多数的语言设计者不一样。

黑客的看法其实比语言设计者的更重要。编程语言不是数学定理,而是一种工具,为了便于使用,它们才被设计出来。所以,设计编程语言的时候必须考虑到人类的长处和短处,就像设计鞋子的时候必须符合人类的脚型。如果鞋子穿上去不舒服,无论它的外形多么优美,多么像一件艺术品,你也只能把它当作一双坏鞋。

大多数程序员也许无法分辨语言的好坏。但是,这不代表优秀的编程语言会被埋没,专家级黑客一眼就能认出它们,并且会拿来使用。虽然他们人数很少,但就是这样一小群人写出了人类所有优秀软件。他们有着巨大的影响力,他们使用什么语言,其他程序员往往就会跟着使用。老实说,很多时候这种影响力更像是一种命令,对于其他程序员来说,专家级黑客就像自己的老板或导师,他们说哪种语言好用,自己就会乖乖地跟进。

专家级黑客的看法不是决定一种语言流行程度的唯一因素,某些古老的软件(Fortran和Cobol的情况)和铺天盖地的广告宣传(Ada和Java的情况)也会起到作用。但是,我认为从长期来看,专家级黑客的看法是最重要的因素。只要有了达到“临界数量”(critical mass)的最初用户和足够长的时间,一种语言可能就会达到应有的流行程度。而流行本身又会使得这种优秀的语言更加优秀,进一步拉大它与平庸语言之间的好坏差异,因为使用者的反馈总是会导致语言的改进。你可以想一下,所有流行的编程语言从诞生至今的变化有多大。Perl和Fortran是极端的例子,但是甚至就连Lisp都发生了很大的变化。

所以,即使不考虑语言本身的优秀是否能带动流行,我想单单流行本身就肯定会使得这种语言变得更好,只有流行才会让它保持优秀。编程语言的最高境界一直在发展之中。虽然语言的核心功能就像大海的深处,很少有变化,但是函数库和开发环境之类的东西就像大海的表面,一直在汹涌澎湃。

当然,黑客必须先知道这种语言,才可能去用它。他们怎么才能知道呢?就是从其他黑客那里。所以不管怎样,一开始必须有一群黑客使用这种语言,然后其他人才会知道它。我不知道“一群”的最小数量是多少,多少个黑客才算达到“临界数量”呢?如果让我猜,我会说20人。如果一种语言有20个独立用户,就意味这20个人是自主决定使用这种语言的,我觉得这就说明这种语言真的有优点。

达到这一步并非易事。如果说用户数从0到20比从20到1000更困难,我也不会感到惊讶。发展最早的20个用户的最好方法可能就是使用特洛伊木马:你让人们使用一种他们需要的应用程序,这个程序偏巧就是用某种新语言开发的。

外部因素

我们得先承认,确实有一个外部因素会影响到语言的流行。一种语言必须是某一个流行的计算机系统的脚本语言(scripting language),才会变得流行。Fortran和Cobol是早期IBM大型机的脚本语言。C是Unix的脚本语言,后来的Perl和Python也是如此。Tcl是Tk的脚本语言,Visual Basic是Windows的脚本语言,(某种形式的)Lisp是Emacs的脚本语言,PHP是网络服务器的脚本语言,Java和JavaScript是浏览器的脚本语言。

编程语言不是存在于真空之中。“编程”其实是及物动词,黑客一般都是为某个系统编程,在现实中,编程语言总是与它们依附的系统联系在一起的。所以,如果你想设计一种流行的编程语言,就不能只是单纯地设计语言本身,还必须为它找到一个依附的系统,而这个系统也必须流行。除非你只想用自己设计的语言取代那个系统现有的脚本语言。

这种情况导致的一个结果就是,无法以一种语言本身的优缺点评判这种语言。另一个结果则是,只有当一种语言是某个系统的脚本语言时,它才能真正成为编程语言。如果你对此很吃惊,觉得不公平,那么我会跟你说不必大惊小怪。这就好比大家都认为,如果一种编程语言只有语法规则,没有一个好的实现(implementation),那么它就不能算完整的编程语言。这些都是很正常很合理的事情,编程语言本来就该如此。

当然,编程语言本来就需要一个好的实现,而且这个实现必须是免费的。商业公司愿意出钱购买软件,但是黑客作为个人不会愿意这样做,而你想让一种语言成功,恰恰就是需要吸引黑客。

编程语言还需要有一本介绍它的书。这本书应该不厚,文笔流畅,而且包含大量优秀的范例。布赖恩 · 柯尼汉和丹尼斯 · 里奇合写的《C程序设计语言》(C Programming Language)就是这方面的典范。眼下,我大概还能再加一句,这一类书籍之中必须有一本由O'Reilly公司出版发行。这正在变成是否能吸引黑客的前提条件了。

编程语言还应该有在线文档。事实上,在线文档可以当作一本书来写,但是目前它还无法取代实体书。实体书并没有过时,它们读起来很方便,而且出版社对书籍内容的审核是一种很有用的质量保证机制(虽然做得很不完美)。书店则是程序员发现和学习新语言的最重要的场所之一。

简洁

假定你的语言已经能够满足上面三项条件——一种免费的实现,一本相关书籍,以及语言所依附的计算机系统——那么还需要做什么才能使得黑客喜欢上你的语言?

黑客欣赏的一个特点就是简洁。黑客都是懒人,他们同数学家和现代主义建筑师一样,痛恨任何冗余的东西或事情。有一个笑话说,黑客动手写程序之前,至少会在心里盘算一下哪种语言的打字工作量最小,然后就选择使用该语言。这个笑话其实与真实情况相差无几。就算这真的是个笑话,语言的设计者也必须把它当真,按照它的要求设计语言。

简洁性最重要的方面就是要使得语言更抽象。为了达到这一点,首先你设计的必须是高级语言,然后把它设计得越抽象越好。语言设计者应该总是看着代码,问自己能不能使用更少的语法单位把它表达出来。如果你有办法让许多不同的程序都能更简短地表达出来,那么这很可能意味着你发现了一种很有用的新抽象方法。

不要觉得为用户着想就是让他们使用像英语一样又长又啰嗦的语法。这是不正确的做法,Cobol就是因为这个毛病而声名狼藉。如果你让黑客像下面这样求和:

add x to y giving z

而不是写成:

z=x+y

那么你就是在侮辱黑客的智商,或者自己作孽了。

简洁性是静态类型语言的力所不及之处。不考虑其他因素时,没人愿意在程序的头部写上一大堆的声明语句。只要计算机可以自己推断出来的事情,都应该让计算机自己去推断。举例来说,hello-world本应该是一个很简单的程序,但是在Java语言中却要写上一大堆东西,这本身就差不多可以说明Java语言设计得有问题了。1

1hello-world程序的唯一作用就是显示出“Hello, world!”这句话。使用Java语言,你需要这样写:

public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}

如果你从来没有接触过编程,看到上面的代码可能会很奇怪,让计算机显示一句话为什么要搞得这么复杂?有意思的是,资深程序员的反应与你一样。

单个的语法单位也应该很简短。Perl和Common Lisp在这方面是两个不同的极端。Perl的语法单位很短,导致它的代码可以拥挤得让人无法理解,而Common Lisp内置运算符的名称则长得可笑。Common Lisp的设计者们可能觉得文本编辑器会帮助用户自动填写运算符的长名称。但是这样做的代价不仅是增加了打字的工作量,还包括提高了阅读代码的难度,以及占用了更多的显示器空间。

可编程性(Hackability)

对黑客来说,选择编程语言的时候,还有一个因素比简洁更重要,那就是这种语言必须能够帮助自己做到想做的事。在编程语言的历史上,防止程序员做出“错误”举动的措施多得惊人。这是语言设计者很自以为是的危险举动,他们怎么知道程序员该做什么不该做什么?我认为,语言设计者应该假定他们的目标用户是一个天才,会做出各种他们无法预知的举动,而不是假定目标用户是一个笨手笨脚的傻瓜,需要别人的保护才不会伤到自己。如果用户真的是傻瓜,不管你怎么保护他,他还是会搬起石头砸自己的脚。你也许能够阻止他引用另一个模块中的变量,但是你没法防止他日日夜夜不知疲倦地写出结构混乱的程序去解决完全错误的问题。

优秀程序员经常想做一些既危险又令人恼火的事情。所谓“令人恼火”,我指的是他们会突破设计者提供给用户的外部语义层,试着控制某些高级抽象的语言内部接口。比如,黑客喜欢破解,而破解就意味着深入内部,揣测原始设计者的意图。

你应该敞开胸怀,欢迎这种揣测。对于制造工具的人来说,总是会有用户以违背你本意的方式使用你的工具。如果你制造的是编程语言这样高度组合的系统,那就更是如此了。许多黑客会用你做梦也想不到的方式改动你的语法模型。我的建议就是,让他们这样干吧,而且应该为他们创造便利,尽可能多地把语言的内部暴露在他们面前。

其实,黑客并不会彻底颠覆你的工具,在一个大型程序中,他可能只是对语言改造一两个地方。但是,改动多少地方并不重要,重要的是他能够对语言进行改动。这可能不仅有助于解决一些特殊的问题,还会让黑客觉得很好玩。黑客改造语言的乐趣就好比外科医生摆弄病人内脏的乐趣,或者青少年喜欢用手挤破青春痘的那种感觉。2至少对男生来说,某些类型的破坏非常刺激。针对青年男性读者的Maxim杂志每年出版一本特辑,里面一半是美女照片,另一半是各种严重事故的现场照片。这本杂志非常清楚它的读者想看什么。

2在《神经外科医生手记》(When the Air Hits Your Brain)一书中,神经外科医生弗托塞克讲述了住院总医生戈雷的一段话,内容关于外科医生与内科医生的区别。
戈雷和我要了一个大比萨,找了一张空桌子坐下。他点起一根香烟,说:“那些内科医生真是令人讨厌,总是喜欢谈论一辈子只能遇到一次的病例。这就是他们的问题,他们只喜欢古怪的东西,讨厌普通的常见病例。这就是我们和他们的区别。你看,我们喜欢腰椎间盘突出,觉得像比萨一样又大又好吃,但是他们看到高血压就憎恨不已……”
很难把腰椎间盘突出与又大又好吃联系在一起,但是,我想我知道他们指的是什么。我经常觉得某个bug非常诱人,一定要追踪下去。不是程序员的人很难想象bug有什么好玩的。一切正常当然很好,但是不可否认,能够抓到某些bug会让人兴奋到极点。

一种真正优秀的编程语言应该既整洁又混乱。“整洁”的意思是设计得很清楚, 内核由数量不多的运算符构成,这些运算符易于理解,每一个都有很完整的独立用途。“混乱”的意思是它允许黑客以自己的方式使用。C语言就是这样的例子,早期的Lisp语言也是如此。真正的黑客语言总是稍微带一点放纵不羁、不服管教的个性。

优秀的编程语言所具备的功能,应该会使得言必称“软件工程”的人感到非常不满、频频摇头。与黑客语言形成鲜明对照的就是像Pascal那样的语言,它是井然有序的模范,非常适合教学,但是除此之外就没有很大用处了。

一次性程序

为了吸引黑客,一种编程语言必须善于完成黑客想要完成的各种任务。这意味着它必须很适合开发一次性程序。这一点可能出乎很多人的意料。

所谓一次性程序,就是指为了完成某些很简单的临时性任务而在很短时间内写出来的程序。比如,自动完成某些系统管理任务的程序,或者(为了某项模拟任务)自动生成测试数据的程序,以及在不同格式之间转化数据的程序等。令人吃惊的是,一次性程序往往不是真的只用一次,就像二战期间很多美国大学造的一大批临时建筑后来都成了永久建筑。许多一次性程序后来也都变成了正式的程序,具备了正式的功能和外部用户。

我有一种预感,最优秀的那些大型程序就是这样发展起来的,而不是像胡佛水坝那样从一开始就作为大型工程来设计。一下子从无到有做出一个大项目是很恐怖的一件事。当人们接手一个巨型项目时,很容易被它搞得一蹶不振。最后,要么是项目陷入僵局,要么是做出来一个规模小、性能差的东西。你想造一片闹市,却只做出一家商场;你想建一个罗马,却只造出一个巴西利亚;你想发明C语言,却只开发出Ada。

开发大型程序的另一个方法就是从一次性程序开始,然后不断地改进。这种方法比较不会让人望而生畏,程序在不断的开发之中逐渐进步。一般来说,使用这种方法开发程序,一开始用什么编程语言,就会一直用到最后,因为除非有外部政治因素的干预,程序员很少会中途更换编程语言。所以,我们就有了一个看似矛盾的结论:如果你想设计一种适合开发大型项目的编程语言,就必须使得这种语言也适合开发一次性程序,因为大型项目就是从一次性程序演变而来的。

Perl就是一个鲜明的例子。它不仅仅设计成适合开发一次性程序,而且它本身就很像一次性程序。最初的Perl只是好几个生成表格的工具收集在一起而已。后来程序员用它写一次性程序,当那些程序逐渐发展壮大后,Perl才随之发展成了一种正式的编程语言。到了Perl 5,这种语言才适合开发重要的程序,但是在此之前它已经广为流行了。

什么样的语言适合写一次性程序?首先,它必须很容易装备。一次性程序是你只想在一小时内写出来的程序,所以它不应该耗费很多时间安装和配置,最好已经安装在你的电脑上了。它必须是想用就用的。C语言可以想用就用,因为它是操作系统的一部分;Perl可以想用就用,因为它本来就是一种系统管理工具,操作系统已经默认安装它了。

很容易装备不仅仅指很容易安装或者已经安装,还指很容易与使用者互动。一种有命令行界面、可以实时反馈的语言就具有互动性,那些必须先编译后使用的语言就不具备互动性。受欢迎的编程语言应该是前者,具有良好的互动性,可以快速得到运行结果。

一次性程序的另一个特点就是简洁。对黑客来说,这一点永远有吸引力。如果考虑到你最多只打算在这个程序上耗费一个小时,这一点就更重要了。

函数库

简洁性的最高形式当然是有人已经帮你把程序写好,你只要运行就可以了。函数库就是别人帮你写好的程序,所以它是编程语言的另一个重要特点,并且我认为正在变得越来越重要。Perl就赢在它具有操作字符串的巨大函数库。这类函数库对一次性程序特别重要,因为开发一次性程序的原始目的往往就是转化或提取字符串。许多Perl程序的原型可能就是把几个函数库调用放在一起。

我认为,未来50年中,编程语言的进步很大一部分与函数库有关。未来的函数库将像语言内核一样精心设计。优秀函数库的重要性将超过语言本身。某种语言到底是静态类型还是动态类型、是面向对象还是函数式编程,这些都不如函数库重要。那些习惯用变量类型考虑问题的语言设计者可能会对这种趋势感到不寒而栗。这不等于把语言设计降到开发应用程序的层次吗?哦,真是太糟了。但是别忘了,编程语言是供程序员使用的,而函数库就是程序员需要的东西。

设计优秀的函数库是很难的,并不只是写一大堆代码而已。一旦函数库数量变得太多,找到一个你需要的函数有时候还不如自己动手写来得快。函数库的设计基础与语言内核一样,都是一个小规模的运算符集合。函数库的使用应该符合程序员的直觉,让他可以猜得出哪个函数能满足自己的需要。

效率

众所周知,好的编程语言生成的代码有较快的运行速度。但是实际上,我觉得代码的运行速度不是编程语言的设计者能够控制的。高德纳很久以前就指出,运行速度只取决于一些关键的瓶颈。而在编程实践中,许多程序员都已经注意到自己很容易搞错瓶颈到底在哪里。

所以,编程时提高代码运行速度的关键是使用好的性能分析器(profiler),而不是使用其他方法,比如精心选择一种静态类型的编程语言。为了提高运行速度,并没有必要每个函数的每个参数类型都声明清楚,你只需要在瓶颈处声明清楚参数类型就可以了。所以,更重要的是你需要能够找出瓶颈到底在什么地方。

人们在使用非常高级的语言(比如Lisp)时,经常抱怨很难知道哪个部分对性能的影响比较大。可能确实如此,如果你使用一种非常抽象的语言,这也许是无法避免的。不管怎样,我认为一个好的性能分析器会解决这个问题,虽然这方面还有很长的路要走,但是未来你可以快速知道程序每个部分的时间开销。

这个问题一部分源于沟通不畅。语言设计者喜欢提高编译器的速度,认为这是对自己技术水平的考验,而最多只把性能分析器当作一个附送给使用者的赠品。但是在现实中,一个好的性能分析器对程序的帮助可能大于编译器的作用。这里又一次反映出语言设计者与用户之间发生了脱节,前者竭尽全力想要解决的问题其实方向不甚正确。

让性能分析器自动运行可能是一个好主意。它自动告诉程序员每个部分的性能,而不是非要等到程序员手动运行后才能知道。比如,当程序员编辑源码的时候,代码编辑器能够实时用红色显示瓶颈的部分。另一个方法应该是设法显示正在运行的程序的情况,这对互联网软件尤其重要,因为服务器上有很多程序同时运行,它们都需要你密切关注。自动运行的性能分析器用图形实时显示程序运行时的内存状况,甚至可以发出声音,表示出现了问题。

出现问题时,声音是很好的提示。我们在Viaweb搞了一块很大的面板,上面有各种各样的仪表盘,用来显示服务器的状况。仪表盘的指针由微型马达驱动,每当马达旋转的时候,就会发出一阵轻微的噪音。在我的工位没法看到仪表盘,但是只要我听到声音,就能立刻知道服务器出现了问题。

性能分析器甚至有可能自动找出不合理的算法。如果将来有人发现某种形式的内存访问是不合理算法的信号,我不会感到很惊讶。如果有一个小人儿可以钻进计算机看看我们的程序是怎么运行的,他可能会变成一个忙碌又悲惨的可怜虫,就像那些为政府跑腿的小人物。我总觉得自己用处理器做了很多无用功,但是一直没有找到能够看出程序是怎样浪费运算能力的好办法。

现在有一些语言先编译成字节码(byte code),然后再由解释器执行。这样做主要是为了让代码容易移植到不同的操作系统,但是这也可以变成一项很有用的功能。让字节码成为语言的正式组成部分,允许程序员在瓶颈处内嵌字节码,这可能是一个不错的主意。然后,针对这部分字节码的优化也就变得可以移植了。

正如许多最终用户已经意识到的,运行速度的概念正在发生变化。随着互联网软件的兴起,越来越多的程序主要不是受限于计算机的运算速度,而是受限于I/O的速度。加快I/O速度将是很值得做的一件事。在这方面,编程语言也能起到作用,有些措施是显而易见的,比如采用简洁、快速、格式化输出的函数,还有些措施则需要深层次的结构变化,比如采用缓存和持久化对象(persistent object)。

用户关心的是反应时间(response time),但是软件的另一种效率正在变得越来越重要,那就是每个处理器能够同时支持的用户数量。未来许多有趣的应用程序都将是运行在服务器端的互联网软件,所以每台服务器能够支持的用户数量就成了软件业者的关键问题。互联网软件的资本支出就取决于这个指标。

许多年以来,大多数面向最终用户的程序都不太关心效率。软件开发者总是假设用户桌面电脑的运算能力会不断增长,所以不用刻意提高软件的效率。帕金森定律3被证明与摩尔定律一样颠扑不破。软件不断膨胀,消耗光所有可以得到的资源。这一切将随着互联网软件的出现发生改变,因为硬件和软件现在捆绑在一起供应。对于那些提供互联网软件的公司来说,将每台服务器支持的用户数量最大化会对降低成本产生巨大影响。

3帕金森定律(Parkinson's Law)的一种原始表达形式是“工作总是到最后一刻才会完成”,后来引申到计算机领域就变成了“数据总是会填满所有空间”,更一般性的总结则是“对一种资源的需求总是会消耗光这种资源的所有供应”。——译者注

在一些应用程序中,处理器的运算能力是瓶颈,那么最重要的优化对象就是软件的运行速度。但是,一般情况下内存才是瓶颈,你能够同时支持的用户数量取决于用户数据所消耗的内存。编程语言在这方面也能发挥作用,对线程的良好支持将使得所有用户共享同一个内存堆(heap)。持久化对象和语言内核级别的延迟加载(lazy loading)支持也有助于减少内存需求。

时间

一种编程语言要想变得流行,最后一关就是要经受住时间的考验。没人想用一种会被淘汰的语言编程,这方面已经有很多前车之鉴了。所以,大多数黑客往往会等上几年,看看某一种新语言的势头,然后才真正考虑使用它。

新事物的发明者通常对这个发现很震惊,他们没想到人们居然这样对待发明创造。但是,让别人相信一种新事物是需要时间的。我有一个朋友,他的客户第一次提出某种需求时,他很少理会。因为他知道人们有时候会想要自己并不真正需要的东西。为了避免浪费时间,只有当客户第三次或第四次提出同样的需求时,他才认真对待。这个时候客户可能已经很不高兴了,但是这至少保证他们提出的需求应该就是他们真正需要的东西。

大多数人接触新事物时都学会了使用类似的过滤机制。甚至有时要听到别人提起十遍以上他们才会留意。这样做完全是合理的,因为大多数的热门新商品事后被证明都是浪费时间的噱头,没多久就消失得无影无踪。虚拟现实建模语言VRML刚诞生时曾经轰动一时,但是我决定等到一两年后再去学习它,结果一两年后已经没有学习的必要了,因为市场已经把它遗忘了。

所以,发明新事物的人必须有耐心,要常年累月不断地做市场推广,直到人们开始接受这种发明。我们就耗费了好几年才使得客户明白Viaweb不需要下载安装就能使用。不过,好消息是,简单重复同一个信息就能解决这个问题。你只需要不停地重复同一句话,最终人们将会开始倾听。人们真正注意到你的时候,不是第一眼看到你站在那里,而是发现过了这么久你居然还在那里。

新事物的发展改进一般也需要很长时间。大多数技术在诞生后都逐渐发生了巨大的变化,编程语言更是如此。诞生头几年,一小批早期使用者比其他因素更能促进技术发展。早期使用者都是行家,要求也很高,能够很快找出你的技术中存在的缺点。而且,如果你的用户只有很少几个人,你就能够与他们所有人保持密切接触。只要不断改进你的系统,即使给用户造成了损失,早期使用者也会对你宽容大度的。

新技术被市场接纳的方式有两种,一种是自然成长式,另一种是大爆炸式。自然成长式的一个例子就是在车库里白手起家、自力更生的创业者。几个好朋友埋头工作,在外界毫不知晓的情况下开发出某种新技术。他们把它推向市场,没有任何宣传,最初的用户寥寥无几(但是热心程度无与伦比)。创业者持续改进新技术,与此同时,通过口碑效应,用户数量不断增长。在创业者不经意间,他们已经壮大起来了。

大爆炸式的例子是有风险资本支持、在市场上大张旗鼓宣传的创业公司。他们急急忙忙地开发一个产品,推向市场的时候大肆曝光,立刻就获得了一大批使用者(至少他们希望如此)。

一般来说,车库里的创业者会妒忌大爆炸式的创业公司。后者的主导人物个个光彩照人、自信非凡,深受风险资本商的追捧。他们什么都买得起,在公关公司配合产品推出的宣传活动中,他们自己也附带成为了明星人物。自然成长式的创业者坐在自家车库里,觉得自己又穷又可怜。但是我想他们不必难过。最终来看,自然成长式会比大爆炸式产生更好的技术,能为创始人带来更多的财富。如果你研究一下目前的主流技术,就会发现大部分都是源于自然成长式。

这种模式不仅存在于商业公司,还存在于科研活动中。Multics操作系统和Ada语言是大爆炸式项目,现在都已经销声匿迹了,而它们的继承者Unix和C语言则是自然成长式项目。

再设计

著名散文家E.B.怀特说过,“最好的文字来自不停的修改”。所有优秀作家都知道这一点,它对软件开发也适用。设计一样东西,最重要的一点就是要经常“再设计”,编程尤其如此,再多的修改都不过分。

为了写出优秀软件,你必须同时具备两种互相冲突的信念。一方面,你要像初生牛犊一样,对自己的能力信心万丈;另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度。在你的大脑中,有一个声音说“千难万险只等闲”,还有一个声音却说“早岁哪知世事艰”。

这里的难点在于你要意识到,实际上这两种信念并不矛盾。你的乐观主义和怀疑倾向分别针对两个不同的对象。你必须对解决难题的可能性保持乐观,同时对当前解法的合理性保持怀疑。

做出优秀成果的人,在做的过程中常常觉得自己做得不够好。其他人看到他们的成果觉得棒极了,而创造者本人看到的都是自己作品的缺陷。这种视角的差异并非偶然,因为只有对现状不满,才会造就杰出的成果。

如果你能平衡好希望和担忧,它们就会推动项目前进,就像自行车在保持平衡中前进一样。在创新活动的第一阶段,你不知疲倦地猛攻某个难题,自信一定能够解决它。到了第二阶段,你在清晨的寒风中看到自己已经完成的部分,清楚地意识到存在各种各样的缺陷。此时,只要你对自己的怀疑没有超过你对自己的信心,就能够坦然接受这个半成品,心想不管多难我还是可以把剩下的部分做完。

让这两股相反的力量保持平衡是很难的。初出茅庐的年轻黑客都很乐观,自以为做出了伟大的产品,从不反思和改进。上了年纪的黑客又太不自信,甚至故意回避一些挑战性很强的项目。

任何措施,只要能让“再设计”周而复始地进行下去,就都是可取的。文章可以修改到你满意为止,但是软件的修改通常来说可以无休止地进行下去。文章的读者不可能抱怨修改后新增加的内容让他们前后的思想产生了不协调,但是软件的使用者就会抱怨修改后的版本有不兼容问题。

用户是一把双刃剑。他们推动语言的发展,但也使得你不敢对语言进行大规模改造。所以,一开始的时候要精心选择用户,避免使用者过快增长。发展用户就像一种优化过程,明智的做法就是放慢速度。一般情况下,用户比较少意味着你任何时候都可以加大修改的力度。这时,对语言规格做出改变就像撕绷带,当你感到痛苦的一瞬间,痛苦就已经成为了回忆。如果用户数量庞大,修改语言带来的痛苦就将持续很长时间。

大家都知道,让一个委员会负责设计语言是非常糟糕的主意。委员会只会做出恶劣的设计。但是我觉得,委员会最大的问题在于他们妨碍了“再设计”。在委员会的主持下,修改一种语言是非常麻烦的事,没有人愿意自讨苦吃。而且,即使大多数成员不喜欢某种做法,委员会最后的决定往往还是维持现状。

就算委员会只有两个人,还是会妨碍“再设计”,典型例子就是软件内部的各个接口由不同的人负责。这时除非两个人都同意改变接口,否则接口就无法改变。因此现实中,尽管软件功能越来越强大,内部接口却往往一成不变,成为整个系统中拖后腿的部分。

一种可能的解决方法是,将软件内部的接口设计成垂直接口而不是水平接口。这意味着软件内部的模块是一个个垂直堆积起来的抽象层,层与层之间的接口完全由其中的一层控制。如果较高的一层使用了较低的一层定义的语言,那么接口就由较低的一层控制;如果较低的一层从属于较高的一层,那么接口就由较高的一层控制。

梦寐以求的编程语言

让我们试着描述黑客心目中梦寐以求的语言来为以上内容做个小结。这种语言干净简练,具有最高层次的抽象和互动性,而且很容易装备,可以只用很少的代码就解决常见的问题。不管是什么程序,你真正要写的代码几乎都与你自己的特定设置有关,其他具有普遍性的问题都有现成的函数库可以调用。

这种语言的句法短到令人生疑。你输入的命令中,没有任何一个字母是多余的,甚至用到Shift键的机会也很少。

这种语言的抽象程度很高,使得你可以快速写出一个程序的原型。然后,等到你开始优化的时候,它还提供一个真正出色的性能分析器,告诉你应该重点关注什么地方。你能让多重循环快得难以置信,并且在需要的地方还能直接嵌入字节码。

这种语言有大量优秀的范例可供学习,而且非常符合直觉,你只需花几分钟阅读范例就能领会应该如何使用此种语言。你偶尔才需要查阅操作手册,它本身很薄,里面关于限定条件和例外情况的警告寥寥无几。

这种语言的内核很小,但很强大。各个函数库高度独立,而且和内核一样经过精心设计,它们都能很好地协同工作。语言的每个部分就像精密照相机的各种零件一样完美契合,不需要为了兼容性问题放弃或者保留某些功能。所有函数库的源码都很容易得到。这种语言能够很轻松地与操作系统和用其他语言开发的应用程序对话。

这种语言以层的方式构建。较高的抽象层透明地构建在较低的抽象层之上。如果需要的话,你可以直接使用较低的抽象层。

除了一些绝对必要隐藏的东西,这种语言的所有细节对使用者都是透明的。它提供的抽象能力只是为了方便你的开发,而不是为了强迫你按照它的方式行事。事实上,它鼓励你参与它的设计,给你提供与语言创造者平等的权力。你能够对它的任何部分加以改变,甚至包括它的语法。它尽可能让你自己定义的部分与它本身定义的部分处于同等地位。这种梦幻般的编程语言不仅开放源码,更开放自身的设计。

 

{%}

不难发现,我们身边的每件东西都逐渐与计算机关联起来了。打字机被计算机取代,手机变成了一台小型的计算机,照相机也是如此。不久以后,电视机和录像机也将会变成计算机网络的一部分。而汽车芯片的运算能力甚至超过了1970年需要一整间屋子才放得下的大型机。信件、百科全书、报纸乃至一些社区商店都正在被互联网取代。下一个改变会是什么?《黑客与画家:硅谷创业之父Paul Graham文集》带领我们探究黑客的世界,了解这些人的爱好和动机。Paul Graham妙笔生花,旁征博引历史上的事件,带领读者快速地游览了他所谓的“智力西部”,给人以深刻启发。本文节选自《黑客与画家:硅谷创业之父Paul Graham文集》

欢迎你,很高兴你选择了Java 8

{%}

作者/ Benjamin J. Evans

Benjamin是jClarity公司的联合创始人,伦敦Java用户组的组织者,JCP执行委员会委员。Java Champion和JavaOne Rockstar荣誉得主。他著有《Java技术手册》,与人合著有《Java程序员修炼之道》。他经常就Java平台、性能、并发和相关主题发表公开演讲。

欢迎学习 Java 8。也许应该说欢迎你回来。你可能是从其他语言转到这个生态系统的,也可能这是你学习的第一门编程语言。不管你是如何到达这里的,都要欢迎你。很高兴你选择了 Java。

Java 是一个强大且通用的编程环境,是世界上使用范围最广的编程语言之一,在商务和企业计算领域取得了极大的成功。

本文介绍 Java 语言(供程序员编写应用)、Java 虚拟机(用来运行应用)和 Java 生态系统(为开发团队提供很多有价值的编程环境)。

我们会先简要介绍 Java 语言和虚拟机的历史,然后说明 Java 程序的生命周期,最后厘清 Java 和其他环境之间一些常见的疑问。

本文最后会介绍 Java 的安全性,还会讨论一些安全编程相关的话题。

Java语言、JVM和生态系统

Java 编程环境出现于 20 世纪 90 年代末,由 Java 语言和运行时组成。运行时也叫 Java 虚拟机(Java Virtual Machine,JVM)。

Java 刚出现时,这种分离方式很新奇,但最近软件开发的趋势表明,这已经变成了通用做法。值得一提的是微软的 .NET 环境,它比 Java 晚几年出现,但沿用了非常类似的平台架构方式。

微软的 .NET 平台和 Java 相比有个重要的区别,人们都觉得 Java 是相对开放的生态系统,有多个开发方。在 Java 的演进过程中,这些开发方在合作的同时也有竞争,不断推进 Java 的技术发展。

Java 成功的主要原因之一是,整个生态系统是个标准的环境。这意味着组成 Java 环境的各种技术都有规范。这些标准让开发者和客户相信,自己所用的技术能和其他组件兼容,即便来自不同的技术提供方也不怕。

Java 目前归甲骨文公司所有(甲骨文收购了发明 Java 的太阳计算机系统公司,以下简称 Sun)。红帽、IBM、惠普、SAP、苹果和富士通等公司也大量参与了 Java 标准技术的实现。

Java 也有开源版本,叫 OpenJDK,由多家公司合作开发。

其实,Java 由多个不同但相互联系的环境和规范组成,包括 Java 移动版(Java ME)、Java 标准版(Java SE)和 Java 企业版(Java EE)。

后面会详细说明标准,现在先介绍 Java 语言和 JVM。这是两个不同但互有关联的概念。

Java语言是什么

Java 程序的源码使用 Java 语言编写。Java 是人类可读的编程语言,基于类,而且面向对象,比较易读易写(偶尔有点啰嗦)。

Java 有意识地降低了教、学成本,参考了 C++ 等语言的行业经验,尽量删除了复杂的功能,但保留了“前辈”编程语言的精粹。

总的来说,Java 的目的是为企业开发商业应用提供坚实稳定的基础。

作为一门编程语言,Java 的设计相对保守,而且改动频率低。这么做是有意保护企业对 Java 技术的投入。

Java 语言自 1996 年发布之后,一直在不断地修订(但没有完全重写)。也就是说,一开始为 Java 选择的设计方式,即 20 世纪 90 年代末采用的那些权宜之计,现在仍旧影响着这门语言。

Java 8 的变动幅度很大,是近十年来罕见的(有些人觉得是 Java 出现以来最大的变动)。lambda 表达式的引入和核心中集合 API 的大幅度改写等,将彻底改变大多数 Java 开发者编写代码的方式。

Java 语言受 Java 语言规范(Java Language Specification,JLS)的约束,这个规范限定了某项功能必须采用某种方式实现。

JVM是什么

JVM 是一个程序,提供了运行 Java 程序所需的运行时环境。如果某个硬件和操作系统平台没有相应的 JVM,就不能运行 Java 程序。

幸好,JVM 被移植到了大多数设备中,机顶盒、蓝光播放器、大型机或许都有适用的 JVM。

Java 程序一般都在命令行中启动,例如:

java <arguments> <program name>

这个命令会在操作系统的一个进程中启动 JVM,提供 Java 运行时环境,然后在刚启动的(空)虚拟机中运行指定的程序。

有一点很重要,你要知道:提供给 JVM 运行的程序不是 Java 语言源码,源码必须转换(或编译)成一种称为 Java 字节码的格式。提供给 JVM 的 Java 字节码必须是类文件格式,其扩展名为 .class。

JVM 是字节码格式程序的解释器,一次只执行字节码中的一个指令。而且,你还要知道,JVM 和用户提供的程序都能派生额外的线程,所以用户提供的程序中可能同时运行着多个不同的函数。

JVM 的设计方式建立在几个早期编程环境的多年发展经验之上,尤其是 C 和 C++,因此有多个目的,这些目的都是为了减轻程序员的负担。

  • 包含一个容器,让应用代码在其中运行。

  • 较之 C/C++,提供了一个安全的执行环境。

  • 代开发者管理内存。

  • 提供一个跨平台的执行环境。

介绍 JVM 时往往都会提到这些目的。

前面介绍 JVM 和字节码解释器时已经提到了第一个目的,即 JVM 是应用代码的容器。

第四个目的有时也说成“一次编写,到处运行”,意思是 Java 类文件可从一个运行平台迁移到另一个平台,只要有可用的 JVM,就能正常运行。

也就是说,Java 程序可以在运行着 OS X 的苹果 Mac 电脑中开发(并转换成类文件),然后把类文件移到 Linux 或微软 Windows(或其他平台)中,无需任何改动,Java 程序依然能运行。

Java 环境被移植到了众多平台中,除了 Linux、Mac 和 Windows 等主流平台外,还支持很多其他平台。Mac、Windows、Linux、Solaris、BSD Unix 和 AIX 等被视为“主流平台”,都算在“大多数实现”的范围之内。

除了上述四个主要目的之外,JVM 还有一个设计方面的考量很少被提及和讨论,即 JVM 使用运行时信息进行自我管理。

20 世纪 70 年代和 80 年代对软件的研究表明,程序运行时的行为有很多有趣且有用的模式无法在编译时推论得出。JVM 是真正意义上第一个利用这项研究结果的主流平台。

JVM 会收集运行时信息,从而对如何执行代码做出更好的决定。也就是说,JVM 能监控并优化运行在其中的程序,而没有这种能力的平台则做不到这一点。

一个典型的例子是,在运行 Java 程序的生命周期中,各组成部分被调用的次数并不都是相同的,有些部分调用的次数远比其他部分多得多。Java 平台使用一种名为 JIT 编译(just-in-time compilation)的技术解决这个问题。

在 HotSpot JVM(Sun 为 Java 1.3 开发的 JVM,现在仍在使用)中,JVM 首先识别程序的哪一部分调用最频繁(这一部分叫“热点方法”),然后跳过 JVM 解释器,直接把这一部分编译成机器码。

JVM 利用可用的运行时信息,让程序的性能比纯粹经解释器执行更高。事实上,很多情况下,JVM 使用的优化措施得到的性能提升,已经超过了编译后的 C 和 C++ 代码。

描述 JVM 必须怎样运行的标准叫 JVM 规范。

Java生态系统是什么

Java 语言易于学习,而且和其他编程语言相比,拥有的抽象更少。JVM 为 Java 语言(或其他语言)的运行提供了坚实的基础,并且它写出的程序性能高且是可移植的。这两种相互联系的技术放在一起,可以让企业放心选择在何处下力发展。

然而,Java 的优势不止于此。自 Java 初期开始,就形成了范围极广的生态系统,里面有大量的第三方库和组件。也就是说,开发团队能从现有的连接器和驱动器中获益良多,从中他们能获得几乎任何能想到的技术,有些收费,有些则开源。

在当下的技术生态系统中,很少出现某个技术组件提供 Java 连接器的情况。不管是传统的关系数据库,还是 NoSQL,或者各种企业级监控系统和消息系统,都能集成到 Java 中。

这些正是企业和大型公司采用 Java 技术的主要驱动力。使用现有的库和组件能释放开发团队的潜能,让开发者作出更好的选择,利用 Java 核心技术实现最佳的开放式架构。

Java和JVM简史

  • Java 1.0(1996年)

    这是 Java 的第一个公开发行版,只包含 212 个类,分别放在八个包中。Java 平台始终关注向后兼容性,所以使用 Java 1.0 编写的代码,不用修改或者重新编译,依旧能在最新的 Java 8 中运行。

  • Java 1.1(1997年)

    这一版 Java 平台是原来的两倍多,并且引入了“内部类”和第一版反射 API。

  • Java 1.2(1998年)

    这是 Java 一个非常重要的版本。这一版 Java 平台是原来的三倍,而且首次出现了集合 API(包括 SetMapList)。1.2 版增加的新功能过多,Sun 不得不把平台重新命名为“Java 2 Platform”。这里的“Java 2”是商标,而不是真实的版本号。

  • Java 1.3(2000年)

    这其实是个维护版本,主要用于修正缺陷,解决稳定性,并提升性能。这一版还引入了 HotSpot Java 虚拟机,这个虚拟机现在还在使用(不过有大量的修改和改进)。

  • Java 1.4(2002年)

    这也是一个重要的版本,增加了一些重要的功能,例如高性能低层 I/O API、处理文本的正则表达式、XML 和 XSLT 库、SSL 支持、日志 API 和加密支持。

  • Java 5(2004年)

    这一版 Java 更新幅度很大,对核心语言做了很多改动,引入了泛型、枚举类型(enum)、注解、变长参数方法、自动装包和新版 for 循环。改动的量非常大,所以不得不修改主版本号,以新的主版本号发布。这一版包含 3562 个类和接口,分别放在 166 个包中。在增加的内容中,值得一提的有并发编程的实用工具、远程管理框架和类,以及 Java 虚拟机本身的监测程序。

  • Java 6(2006年)

    这一版也主要是维护和提升性能,引入了编译器 API,扩展了注解的用法和适用范围,还提供了绑定,允许脚本语言和 Java 交互。这一版还对 JVM 和 Swing GUI 技术进行了缺陷修正和改进。

  • Java 7(2011年)

    这是甲骨文公司接管 Java 后发布的第一个版本,包含语言和平台的多项重要升级。这一版引入了处理资源的 try 语句和 NIO.2 API,让开发者编写的资源和 I/O 处理代码更安全且不易出错。方法句柄 API 是反射 API 的替代品,更简单也更安全,而且打开了动态调用(invokedynamic)的大门(Java 1.0 之后第一种新字节码)。

  • Java 8(2014年)

    这是最新版 Java,变动的幅度是自 Java 5(甚至可能是自 Java 出现)以来最大的一次。这一版引入的 lambda 表达式有望显著提升开发者的效率;集合 API 也升级了,改用 lambda 实现,为此,Java 的面向对象实现方式也发生了根本性变化。其他重要更新包括:实现运行在 JVM 中的 JavaScript(Nashorn),新的日期和时间支持,以及 Java 配置(用于生成不同版本的 Java,尤其适合部署无界面或服务器应用)。

Java程序的生命周期

为了更好地理解 Java 代码是怎么编译和执行的,以及 Java 和其他编程环境的区别,请看图 1 中的流程图。

{%}

图1 Java 代码是怎么编译和加载的

整个流程从 Java 源码开始,经过 javac 程序处理后得到类文件,这个文件中保存的是编译源码后得到的 Java 字节码。类文件是 Java 平台能处理的最小功能单位,也是把新代码传给运行中程序的唯一方式。

新的类文件通过类加载机制载入虚拟机,从而把新类型提供给解释器执行。

常见问题解答

下面我们将解答一些关于 Java 和在 Java 环境中编写的程序的生命周期的最常见问题。

1. 字节码是什么

开发者首次接触 JVM 时,可能认为它是“电脑中的电脑”,然后顺其自然把字节码理解为“内部电脑中 CPU 执行的机器码”或“虚拟处理器执行的机器码”。

其实,字节码和运行于硬件处理器中的机器码不太一样。计算机科学家视字节码为一种“中间表现形式”,处在源码和机器码之间。

字节码的目的是,提供一种能让 JVM 解释器高效执行的格式。

2. javac是编译器吗

编译器一般生成机器码,而 javac 生成的是和机器码不太一样的字节码。不过,类文件有点像对象文件(例如 Windows 中的 .dll 文件,或 Unix 中的 .so 文件),人类肯定读不懂。

在计算机科学理论的术语中,javac 非常像编译器的“前半部分”,它生成的中间表现形式可以进一步处理,生成机器码。

不过,因为类文件的生成是构建过程中单独的一步,类似于 C/C++ 中的编译,所以很多开发者都把运行 javac 的操作称为编译。

我们把“编译”看作一个单独的术语,表示 JIT 编译,因为只有 JIT 编译才会生成机器码。

3. 为什么叫“字节码”

指令码(操作码)只占一个字节(有些操作还可以有参数,即跟随其后的字节流),所以只有 256 个可用的指令。实际上,有些指令用不到,大概只会使用 200 个,而且其中还有一些是最新版 javac 不支持的。

4. 字节码是优化过的吗

Java 平台的早期阶段,javac 会对生成的字节码进行大量优化。后来表明这么做是错的。JIT 编译出现后,重要的方法会被编译成运行速度很快的机器码。之所以要减轻 JIT 编译器的负担,是因为 JIT 编译获得的效果,比字节码优化多很多,而且字节码还要经过解释器处理。

5. 字节码真的与设备无关吗?那字节顺序呢

不管在哪种设备中生成,字节码的格式都是一样的,其中也包括设备使用的字节顺序。如果你想知道,我告诉你,字节码始终使用大字节序(big-endian)。

6. Java是解释性语言吗

JVM 基本上算是解释器(通过 JIT 编译大幅提升性能)。可是,大多数解释性语言(例如 PHP、Perl、Ruby 和 Python)都直接从源码解释程序(一般会从输入的源码文件中构建一个抽象句法树)。而 JVM 解释器需要的是类文件,因此当然需要多一步操作,即使用 javac 编译源码。

7. 其他语言可以在JVM中运行吗

可以。JVM 可以运行任何有效的类文件,因此,Java 之外的语言可以通过两种方式在 JVM 中运行。第一种,提供用于生成类文件的源码编译器(类似于 javac),以类似 Java 代码的方式在 JVM 中运行(Scala 等语言采用的是这种方式)。

Java 之外的语言可以使用 Java 实现解释器和运行时,然后解释该语言使用的源码格式。JRuby 等语言采用的就是这种方式(不过 JRuby 的运行时很复杂,某些情况下能辅助 JIT 编译)。

Java的安全性

Java 的设计始终考虑安全性,因此和很多其他现有系统和平台相比有很大的优势。Java 的安全架构由安全专家设计,而且这个平台发布之后,很多其他安全专家仍在研究和探讨。专家们一致认为,Java 的安全架构坚固牢靠,在设计层面没有任何安全漏洞(至少还没有发现)。

Java 安全模型的基础是,严格限制字节码能表述的操作,例如,不能直接访问内存,因此避免了困扰 C 和 C++ 等语言的一整类安全问题。而且,只要 JVM 加载了不信任的类,就会执行字节码校验操作,从而避免了大量问题。

尽管如此,没有任何系统能保证 100% 的安全性,Java 也不例外。

虽然从理论上讲,设计是牢固的,但安全架构的实现是另外一回事,在某些 Java 实现中,一直都在发现和修补安全缺陷。

不得不说,Java 8 的延期发布,至少部分原因是发现了一些安全问题,必须要投入时间进行修复。

我相信,在实现 Java 虚拟机的过程中始终都会发现(并修正)安全缺陷。

不过,值得注意的是,最近发现的 Java 安全问题大都与桌面技术有密切联系。在日常的服务器端编程方面,Java 仍是当前最安全的通用平台。

Java和其他语言比较

接下来我们将简要列出 Java 平台和其他你可能熟悉的编程环境之间的重要不同点。

Java和C语言比较

  • Java 面向对象,C 面向过程。

  • Java 通过类文件实现可移植性,C 需要重新编译。

  • Java 为运行时提供了全面的监测程序。

  • Java 没有指针,也没有指针相等性运算。

  • Java 通过垃圾回收提供了自动内存管理功能。

  • Java 无法从低层布局内存(没有结构体)。

  • Java 没有预处理器。

Java和C++比较

  • Java 的对象模型比 C++ 简单。

  • Java 默认使用虚分派(virtual dispatch)。

  • Java 始终使用值传递(不过 Java 中的值也能作为对象引用)。

  • Java 不完全支持多重继承。

  • Java 的泛型没 C++ 的模板强大(不过危害性较小)。

  • Java 无法重载运算符。

Java和PHP比较

  • Java 是静态类型语言,PHP 是动态类型语言。

  • Java 有 JIT,PHP 没有(PHP 6 可能会有)。

  • Java 是通用语言,PHP 在网站技术之外很难见到。

  • Java 支持多线程,PHP 不支持。

Java和JavaScript比较

  • Java 是静态类型语言,JavaScript 是动态类型语言。

  • Java 使用基于类的对象,JavaScript 使用基于原型的对象。

  • Java 提供了良好的对象封装,JavaScript 没有提供。

  • Java 有命名空间,JavaScript 没有。

  • Java 支持多线程,JavaScript 不支持。

回应对Java的一些批评

Java 出现在公共视线中已有很长一段时间了,因此,在这些年里受到的批评也相当多。这些批评可以归咎于一些技术缺点,以及第一版过度的市场推广。

不过,有些批评只是技术圈的传言,不是很准确。接下来,我们来看一些常见的抱怨,以及它们在最新版 Java 平台中的状况。

过度复杂

人们经常批评 Java 核心语言过度复杂。即便是 Object o = new Object(); 这样简单的语句,也有重复——赋值符号左右两边都出现了类型 Object。批评人士认为这么做完全是多余的,其他语言都不需要重复声明类型,而且很多辅助功能都不用这么做(例如类型推导)。

这样的说法我不认同。从一开始,Java 的设计目标就是易于阅读(读代码的次数比写代码多很多),许多程序员,尤其是新手,都觉得额外的类型信息有助于阅读代码。

Java 广泛用于企业环境,开发团队往往和运维团队不同。这些额外的信息一般会在处理停机,或者需要维护和修订早就投身其他事务的开发者编写的代码时提供重大帮助。

在最近几个 Java 版本中(7 和后面的版本),语言的设计者已经在尝试回应这些观点,他们寻找可以简化句法复杂度的地方,也更充分地利用类型信息。例如:

// 文件辅助方法
byte[] contents =
  Files.readAllBytes(Paths.get("/home/ben/myFile.bin"));

// 使用菱形句法表示重复的类型信息
List<String> l = new ArrayList<>();

// lambda表达式,简化了Runnable
ExecutorService threadPool = Executors.newScheduledThreadPool(2);
threadPool.submit(() -> { System.out.println("On Threadpool"); });

然而,Java 的总体原则是非常缓慢且谨慎地修改语言,所以这些变化可能无法完全让批评者满意。

变化慢

Java 第一版发布至今已经超过 15 年了,而且在那个时候也没经过完整修订。在这段时间里,很多其他语言(例如微软的 C#)都发布了不向后兼容的版本,而 Java 没这么做,因此受到了部分开发者的批评。

而且,最近几年,Java 语言因为没有及时吸收其他语言中常见的功能而受到严厉批评。

Sun(现在是甲骨文)在语言设计上采取了保守方式,是为了尽量避免把成本和不合理功能的外部效应强加在大量的用户群体身上。很多使用 Java 的公司都为这一技术注入了重资,语言设计者要认真负责,不能影响现有的用户和安装群体。

每一个新语言功能都要审慎考虑,不只是新功能本身,还要考虑它会如何影响语言现有的功能。有时,新功能的影响会超过目及之处,而 Java 的使用范围又如此广泛,因此可能有很多地方会产生意料之外的影响。

功能发布后,如果有问题,几乎无法将其删除。Java 有一些不合理的功能(例如终结机制),在不影响安装群体的情况下,根本无法安全地删除。语言设计者认为,在语言演进的过程中必须极为小心。

话虽如此,但 Java 8 引入的新语言功能向前迈出了一大步,回应了最常见的功能缺失抱怨,应该能为开发者提供他们一直诉求的语言特性。

性能问题

现在仍然有人批评 Java 平台的速度慢,而且所有批评都集中在“平台”上,这或许是最不合理的批评了。

Java 1.3 引入了 HotSpot 虚拟机和 JIT 编译器,而且在随后的 15 年里,一直在革新和改进虚拟机及其性能。现在,Java 平台的速度异常快,经常会在流行的框架性能评测中取胜,甚至打败了编译成本地机器码的 C 和 C++。

针对这方面的批评大都是因为陈旧的记忆,因为以前的某段时间 Java 很慢。Java 使用的大型且不规则延展的架构方式可能也加深了人们对性能低下的印象。

然而,事实上,任何大型架构都需要评测、分析和性能调校,才能得到最好的表现,Java 也不例外。

Java 平台的核心(Java 语言和 JVM)不仅现在是,以后也仍将是开发者可用的速度最快的通用环境。

不安全

2013 年,Java 平台出现了几个安全漏洞,导致 Java 8 的发布日期延后了。其实,在此之前就有人批评 Java 的安全漏洞数量众多。

在这些漏洞中,有很多都涉及 Java 系统的桌面和 GUI 组件,不会影响使用 Java 编写的网站或其他服务器端代码。

所有编程平台都会时不时地出现安全问题,而且很多其他语言的安全漏洞不比 Java 少,只是少有人知罢了。

太注重企业

Java 平台在公司和企业的开发者中使用广泛,因此觉得 Java 太注重企业一点也不奇怪。人们认为 Java 缺少面向社区的语言所具有的自由风格。

其实,Java 一直都是,而且以后仍将是社区和免费或开源软件开发所广泛使用的语言。在 GitHub 和其他项目托管网站中,Java 是最受欢迎的。

而且,使用范围最广的 Java 语言是通过 OpenJDK 实现的。而 OpenJDK 本身就是开源项目,其社区充满活力,一直在不断增长。

 

{%}

《Java技术手册》旨在帮助有经验的Java程序员充分使用Java 7和Java 8的功能,但也可供Java开发新手学习。书中提供了大量示例,演示了如何充分利用现代API和开发过程中的最佳实践。这一版进行了全面更新。第一部分快速准确地介绍了Java编程语言和Java平台。第二部分讨论了核心概念和API,展示了如何在Java环境中解决实际的编程任务。本文节选自《Java技术手册》

Python的禅定一刻

{%}

作者/ Bill Lubanovic

Bill是第一代程序员,曾在Unix、GUI环境下开发软件,上世纪90年代开始Web开发,如今在一家风能高科技公司从事Web可视化工作。

我们从一个小谜题以及它的答案开始。你认为下面这两行的含义是什么?

(Row 1): (RS) K18,ssk,k1,turn work.
(Row 2): (WS) Sl 1 pwise,p5,p2tog,p1,turn.

它们看起来像是某种计算机程序。实际上,这是一个针织图案。更准确地说,这两行描述的是如何编织袜子的足跟部分。对我来说,看懂它们就像让猫看懂《纽约时报》上的填字游戏一样难,但是对我妻子来说却轻而易举。如果你也懂编织,一样可以轻松看懂。

来看另一个例子。虽然你不知道最终会做出什么,但是马上就能明白下面的内容是什么。

  1/2杯黄油或者人造黄油
  1/2杯奶油
  2.5杯面粉
  1茶匙盐
  1汤匙糖
  4杯糊状土豆(冷藏)

确保在加入面粉之前冷藏所有材料。
混合所有材料。
用力揉。
揉成20个球并冷藏。
对于每一个球:
  在布上洒上面粉。
  用擀面杖把球擀成圆饼。
  入锅,炸至棕色。
  翻面继续炸。

即使你不会做饭,应该也能看懂这是一个菜谱:一系列食物原料以及准备工作。这道菜是什么呢?是 lefse,一道和玉米饼很像的挪威美食。做好之后抹上黄油、果酱或者其他你喜欢吃的东西,最后卷起来吃。

编织图案和菜谱有一些共同的特征。

  • 专有名词、缩写以及符号。有些很常见,有些很难懂。

  • 规定专有名词、缩写以及符号的使用方法,也就是它们的语法

  • 一个操作序列,按照顺序进行。

  • 有时需要重复一些操作(循环),比如炸 lefse 的每一面。

  • 有时需要引用其他操作序列(用计算机术语来说就是一个函数)。在菜谱中,你可能需要引用另一个将土豆捣成糊状的菜谱。

  • 假定已经有相关知识。菜谱假定你知道水是什么以及如何烧水。编织图案假定你学过编织并且不会经常扎到手。

  • 一个期望的结果。在我们的例子中分别是袜子和食物,千万不要把它们混在一起哦。

以上这些概念都会出现在计算机程序中。这两个例子的目的是让你知道编程并不像看起来那么高深莫测,其实只是学习一些正确的单词和规则而已。

下面来看看真正的程序。你知道它在做什么吗?

for countdown in 5, 4, 3, 2, 1, "hey!":
    print(countdown)

这其实是一段 Python 程序,会打印出下面的内容:

5
4
3
2
1
hey!

看到了吗?学习 Python 就像看懂菜谱或者编织图案一样简单。此外,你可以在桌子上舒服并且安全地练习编写 Python 程序,完全不用担心被热水烫到或者被针扎到。

Python 程序有一些特殊的单词和符号——forinprint、逗号、冒号、括号以及其他符号。这些单词和符号是语法的重要组成部分。好消息是,Python 的语法非常优秀,相比其他大多数编程语言,学习 Python 需要记住的语法内容很少。Python 语法非常自然,就像一份菜谱一样。

下面的 Python 程序会从一个 Python 列表(list)中选出一条电视新闻的常用语并打印出来:

cliches = [
    "At the end of the day",
    "Having said that",
    "The fact of the matter is",
    "Be that as it may",
    "The bottom line is",
    "If you will",
    ]
print(cliches[3])

程序会打印出第四条常用语:

Be that as it may

一个 Python 列表,比如 cliches,就是一个值序列,可以通过它们相对于列表起始位置的偏移量来访问。第一个值的偏移量是 0,第四个值的偏移量是 3

人们通常从 1 开始数数,所以从 0 开始数似乎很奇怪。用偏移量来代替位置会更好理解一些。

下面这段程序同样会打印出一条引用内容,但是这次是用说话者的人名而不是列表中的位置来进行访问:

quotes = {
    "Moe": "A wise guy, huh?",
    "Larry": "Ow!",
    "Curly": "Nyuk nyuk!",
    }
stooge = "Curly"
print(stooge, "says:", quotes[stooge])

运行这个小程序会打印出:

Curly says: Nyuk nyuk!

quotes 是一个 Python 字典。字典是一个集合,包含唯一(本例中是跟屁虫“Stooge”的名字)及其关联的(本例中是跟屁虫说的话)。使用字典可以通过名字来存储和查找东西,和列表一样非常有用。

常用语的例子中使用方括号([])来创建 Python 列表,跟屁虫的例子中使用大括号({},大括号的英文是 curly bracket,但是大括号和 Curly1 没有任何关系来创建 Python 字典。这些都是 Python 的语法,在之后的内容中你会看到更多语法。

1Curly 是美国乌鸦童子军(Crow Scouts)的一员,乌鸦童子军是美国和印第安人打仗时由印第安人战俘组成的军队。Curly 是小巨角河战役中为数不多的幸存者之一。小巨角河战役是美军和北美势力最庞大的苏族印地安人之间的战争,在这场战争中印第安人歼灭了美国历史上最有名的第七骑兵团,Curly 当时没有参战,他是第一个报告第七骑兵团战败的人,也因此出名。——译者注

现在我们来看另一个完全不同的例子:示例1中的 Python 程序会执行一系列复杂的任务。这个例子的目的是让你了解典型的 Python 程序长什么样。如果你了解其他计算机语言,可以对比一下。

示例1会连接 YouTube 网站并获取当前评价最高的视频的信息。如果 YouTube 返回的是常见的 HTML 文本,那就很难从中挖掘出我们想要的信息。幸运的是,它返回的是 JSON 格式的数据,这种格式可以直接用计算机处理。JSON(JavaScript Object Notation,JavaScript 对象符号)是一种人类可以阅读的文本格式,它描述了类型、值以及值的顺序。JSON 就像一个小型编程语言,使用 JSON 在不同计算机语言和系统之间交换数据已经成为了一种非常流行的方式。

Python 程序可以把 JSON 文本翻译成 Python 的数据结构和你自己创建出来的一样。这个 YouTube 响应包含很多数据,作为演示我只打印出了前 6 个视频的标题。再说一次,这是一个完整的 Python 程序,你自己也可以运行一下。

示例 1:intro/youtube.py

import json
from urllib.request import urlopen
url = "https://gdata.youtube.com/feeds/api.standardfeeds/top_rated?alt=json"
response = urlopen(url)
contents = response.read()
text = contents.decode('utf8')
data = json.loads(text)
for video in data['feed']['entry'][0:6]:
    print(video['title']['$t'])

最后一次运行这个程序得到的输出是:

Evolution of Dance - By Judson Laipply
Linkin Park - Numb
Potter Puppet Pals: The Mysterious Ticking Noise
"Chocolate Rain" Original Song by Tay Zonday
Charlie bit my finger - again !
The Mean Kitty Song

这个 Python 小程序仅仅用了 9 行代码就很好地完成了任务,并且具备很高的可读性。

  • 第 1 行:从 Python 标准库中导入名为 json 的所有代码。

  • 第 2 行:从 Python 标准 urllib 库中导入 urlopen 函数。

  • 第 3 行:给变量 url 赋值一个 YouTube 地址。

  • 第 4 行:连接指定地址处的 Web 服务器并请求指定的 Web 服务

  • 第 5 行:获取响应数据并赋值给变量 contents

  • 第 6 行:把 contents 解码成一个 JSON 格式的文本字符串并赋值给变量 text

  • 第 7 行:把 text 转换为 data——一个存储视频信息的 Python 数据结构。

  • 第 8 行:每次获取一个视频的信息并赋值给变量 video

  • 第 8 行:使用两层 Python 字典(data['feed']['entry'])和切片操作[0:6])。

  • 第 9 行:使用 print 函数打印出视频标题。

视频信息中包含多种你之前见过的 Python 数据结构。

在这个例子中,我们使用了一些 Python 标准库模块(它们是安装 Python 时就已经包含的程序),但是它们并不是最好的。下面的代码使用第三方 Python 软件包 requests 重写了这个例子:

import requests
url = "https://gdata.youtube.com/feeds/api.standardfeeds/top_rated?alt=json"
response = requests.get(url)
data = response.json()
for video in data['feed']['entry'][0:6]:
    print(video['title']['$t'])

新版代码只有 6 行,并且我认为可读性更高。

真实世界中的Python

那么,是否真的值得付出时间和努力来学习 Python 呢?它真的有用吗?实际上,Python 诞生于 1991 年(比 Java 还早),并且一直是最流行的十门计算机语言之一。公司需要雇用程序员来写 Python 程序,包括你每天都会用到的 Google、YouTube、Dropbox、Netflix 和 Hulu 等。我用 Python 开发过许多产品级应用,从邮件搜索应用到商业网站都有。对于发展迅速的组织来说,Python 能极大地提高生产力。

Python 可以应用在许多计算环境下,如下所示:

  • 命令行窗口

  • 图形用户界面,包括 Web

  • 客户端和服务端 Web

  • 大型网站后端

  • (第三方负责管理的服务器)

  • 移动设备

  • 嵌入式设备

Python 程序从一次性脚本到几十万行的系统都有。我们会介绍 Python 在网站、系统管理和数据处理方面的应用,还会介绍 Python 在艺术、科学和商业方面的应用。

Python与其他语言

Python 和其他语言相比如何呢?什么时候该选择什么语言呢?接下来我们会展示一些其他语言的代码片段,这样更直观一些。如果有些语言你从未使用过,也不必担心,你并不需要看懂所有代码(当你看到最后的 Python 示例时,会发现没学过其他语言也不是什么坏事)。

下面的每段程序都会打印出一个数字和一条描述语言的信息。

如果你使用的是命令行或者终端窗口,那你使用的就是 shell 程序,它会读入你的命令、运行并显示结果。Windows 的 shell 叫作 cmd,它会运行后缀为 .bat 的 batch 文件。Linux 和其他类 Unix 系统(包括 Mac OS X)有许多 shell 程序,最流行的称为 bash 或者 sh。shell 有许多简单的功能,比如执行简单的逻辑操作以及把类似 * 的通配符扩展成文件名。你可以把命令保存到名为“shell 脚本”的文件中稍后运行。shell 可能是程序员接触到的第一个程序。它的问题在于程序超过百行之后扩展性很差,并且比其他语言的运行速度慢很多。下面就是一段 shell 程序:

#!/bin/sh
language=0
echo "Language $language: I am the shell. So there."

如果你把这段代码保存为 meh.sh 并通过 sh meh.sh 命令来运行它,就会看到下面的输出:

Language 0: I am the shell. So there.

老牌语言 C 和 C++ 是底层语言,只有极其重视性能时才会使用。它们很难学习,并且有许多细节需要你自己处理,处理不当就可能导致程序崩溃和其他很难解决的问题。下面是一段 C 程序:

#include <stdio.h>
int main(int argc, char *argv[]) {
    int language = 1;
    printf("Language %d: I am C! Behold me and tremble!\n", language);
    return 0;
}

C++ 和 C 看起来很相似,但是特性完全不同:

#include <iostream>
using namespace std;
int main(){
    int language = 2;
    cout << "Language " << language << \
        ": I am C++! Pay no attention to that C behind the curtain!" << \
        endl;
    return(0);
}

Java 和 C# 是 C 和 C++ 的接班人,解决了后者的许多缺点,但是相比之下代码更加冗长,写起来也有许多限制。下面是 Java 代码:

public class Overlord {
    public static void main (String[] args) {
        int language = 3;
        System.out.format("Language %d: I am Java! Scarier than C!\n", language);
    }
}

如果你没写过这些语言的程序,可能会觉得很奇怪:这都是什么东西?有些语言有很大的语法包袱。它们有时被称为静态语言,因为你必须告诉计算机许多底层细节,下面我来解释一下。

语言有变量——你想在程序中使用的值的名字。静态语言要求你必须声明每个变量的类型:它会使用多少内存以及允许的使用方法。计算机利用这些信息把程序编译成非常底层的机器语言(专门给计算机硬件使用的语言,硬件很容易理解,但是人类很难理解)。计算机语言的设计者通常必须进行权衡,到底是让语言更容易被人使用还是更容易被计算机使用。声明变量类型可以帮助计算机发现更多潜在的错误并提高运行速度,但是却需要使用者进行更多的思考和编程。C、C++ 和 Java 代码中经常需要声明类型。举例来说,在上面的例子中必须使用 intlanguage 变量声明为一个整数。(其他类型的存储方式和整数不同,比如浮点数 3.14159、字符以及文本数据。)

那么为什么它们被称为静态语言呢?因为这些语言中的变量不能改变类型。它们是静态的。整数就是整数,永远无法改变。

相比之下,动态语言(也被称为脚本语言)并不需要在使用变量前进行声明。假设你输入 x = 5,动态语言知道 5 是一个整数,因此变量 x 也是整数。这些语言允许你用更少的代码做更多的事情。动态语言的代码不会被编译,而是由解释器程序来解释执行。动态语言通常比编译后的静态语言更慢,但是随着解释器的不断优化,动态语言的速度也在不断提升。长期以来,动态语言的主要应用场景都是很短的程序(脚本),比如给静态语言编写的程序进行数据预处理。这样的程序通常称为胶水代码。虽然动态语言很擅长做这些事,但是如今它们也已经具备了处理大型任务的能力。

许多年来,Perl(http://www.perl.org/)一直是一门万能的动态语言。Perl 非常强大并且有许多扩展库。然而,它的语法非常难用,并且似乎无法阻挡 Python 和 Ruby 的崛起。下面是一段 Perl 代码:

my $language = 4;
print "Language $language: I am Perl, the camel of languages.\n";

Ruby(http://www.ruby-lang.org/)是一门新语言。它借鉴了一些 Perl 的特点,并且因为 Web 开发框架 Ruby on Rails 红遍大江南北。Ruby 和 Python 的许多应用场景相同,选择哪一个通常看个人喜好或者是否有你需要的库。下面是一段 Ruby 代码:

language = 5
puts "Language #{language}: I am Ruby, ready and aglow."

PHP(http://www.php.net/)在 Web 开发领域非常流行,因为它可以轻松结合 HTML 和代码,就像例子中展示的那样。然而,PHP 语言本身有许多缺陷,并且很少被应用在 Web 以外的领域。

<?PHP
$language = 6;
echo "Language $language: I am PHP. The web is <i>mine<i>, I say.\n";
?>

最后是我们的主角,Python:

language = 7
print("Language %s: I am Python. What's for supper?" % language)

为什么选择Python

Python 是一门非常通用的高级语言。它的设计极大地增强了代码可读性,可读性远比听上去重要得多。每个计算机程序只被编写一次,但是会被许多人阅读和修改许多次。提高可读性也可以让学习和记忆更加容易,因此也更容易修改。和其他流行的语言相比,Python 的学习曲线更加平缓,可以让你很快具备生产力,当然,想成为专家还需要深入学习才行。

Python 简洁的语法可以让你写出比静态语言更短的程序。研究证明,程序员每天可以编写的代码行数是有限的——无论什么语言,因此,如果完成同样的功能只需要编写一半长度的代码,生产力就可以提高一倍。对于重视这一点的公司来说,Python 是一个不算秘密的秘密武器。

在顶尖的美国大学中(http://cacm.acm.org/blogs/blog-cacm/176450-python-is-now-the-most-popular-introductory-teaching-language-at-top-us-universities/fulltext),Python 是计算机入门课程中最流行的语言。此外,它也被两千多名雇主(http://blog.codeeval.com/codeevalblog/2014#.U73vaPldUpw=)用来评估编程技能。

当然,它是免费的,就像啤酒和演讲一样。你可以免费用 Python 来编写任何东西并用在任何地方。没人可以一边阅读你的 Python 程序一边说:“这是一个非常棒的小程序,希望不会发生什么意外。”

Python 几乎可以运行在任何地方并且其标准库中有很多有用的软件。

不过,选择 Python 最关键的理由可能出乎你的意料:大家都喜欢它。实际上,大家不只是把 Python 当作一个完成工作的工具,而是非常享受用它编程。在工作中不得不用其他语言时,人们通常会非常想念 Python 的某些特性。这就是 Python 能够胜出的原因。

何时不应该使用Python

Python 并非在所有场合都是最好用的语言。

它并不是默认安装在所有环境中。如果你的电脑上没有 Python,附录 D 会告诉你如何安装。

对于大多数应用来说,Python 已经足够快了,但是有些场合下,它的性能仍然是个问题。如果你的程序会花费大量时间用于计算(专业术语是中央处理器受限),那么可以使用 C、C++ 或者 Java 来编写程序从而提高性能。但是这并不是唯一的选择!

  • 有时候用 Python 实现一个更好的算法(一系列解决问题的步骤)可以打败 C 中的低效算法。Python 对于开发效率的提升可以让你有更多的时间来尝试各种选择。

  • 在许多应用中,程序会因为等待其他服务器的响应而浪费时间。这段时间里 CPU(中央处理单元,计算机中负责所有计算的芯片)几乎什么都不做,因此,静态和动态程序的端到端时间几乎是一样的。

  • Python 的标准解释器用 C 实现,所以可以通过 C 代码进行扩展。

  • Python 解释器变得越来越快。Java 最初也很慢,经过大量的研究和资金投入之后,它变得非常快。Python 并不属于某个公司,因此它的发展会更缓慢一些。

  • 可能你的项目要求非常严格,无论如何努力 Python 都无法达到要求。那么,借用伊恩 · 荷姆在电影《异形》中说过的一句话,我很同情你。通常来说可以选择 C、C++ 和 Java,不过新语言 Go(http://golang.org,写起来像 Python,性能像 C)也是一个不错的选择。

Python 2与Python 3

你即将面临的最大问题是,Python 有两个版本。Python 2 已经存在了很长时间并且预装在 Linux 和 Apple 电脑中。Python 是一门很出色的语言,但是世界上不存在完美的东西。和其他领域一样,在计算机语言中许多问题很容易解决,但是也有一些问题很难解决。后者的难点在于不兼容:使用修复后的新版本编写的程序无法运行在旧的 Python 系统中,旧的程序也无法运行在新的系统中。

Python 的发明者(吉多 · 范 · 罗苏姆,https://www.python.org/~guido)和其他开发者决定把这些困难问题放在一起解决,并把解决后的版本称作 Python 3。Python 2 已经成为过去,Python 3 才是未来。Python 2 的最后一个版本是 2.7,它会被支持很长一段时间,但也就仅此而已,再也没有 Python 2.8 了。新的开发全部会在 Python 3 上进行。

如果你使用的是 Python 2 也不用担心,两者差别不大。最明显的区别在于调用 print 的方式,最重要的区别则是处理 Unicode 字符的方式。流行的 Python 软件需要逐步升级,和常见的“先有鸡还是先有蛋”问题一样。不过,看起来我们现在终于到达了发生转变的临界点。

禅定一刻

每种计算机语言都有自己的风格。在前言中我提到过,你可以用 Python 的方式来表达自己。Python 中内置了一些自由体诗歌,它们简单明了地说明了 Python 的哲学(就我所知,Python 是唯一一个包含这种复活节彩蛋的语言)。只要在交互式解释器中输入 import this,然后按下回车就能看到它们:

>>> import this
《Python之禅》 Tim Peters

优美胜于丑陋
明了胜于隐晦
简洁胜于复杂
复杂胜于混乱
扁平胜于嵌套
宽松胜于紧凑
可读性很重要
即便是特例,也不可违背这些规则
虽然现实往往不那么完美
但是不应该放过任何异常
除非你确定需要如此
如果存在多种可能,不要猜测
肯定有一种——通常也是唯一一种——最佳的解决方案
虽然这并不容易,因为你不是Python之父
动手比不动手要好
但不假思索就动手还不如不做
如果你的方案很难懂,那肯定不是一个好方案
如果你的方案很好懂,那肯定是一个好方案
命名空间非常有用,我们应当多加利用

 

{%}

《Python语言及其应用》介绍Python语言基础知识及其在各个领域的具体应用,基于最新版本3.X。书中首先介绍了Python语言的一些必备基本知识,然后介绍在商业、科研以及艺术领域使用Python开发各种应用的实例。文字简洁明了,案例丰富实用,是一本难得的Python入门手册。本文节选自《Python语言及其应用》

我为什么开发Ruby

{%}

作者/ 松本行弘

松本行弘是Ruby语言的发明人,在1993年发布了Ruby语言的第一个版本,以后一直从事Ruby的设计与开发。2011年加入著名SaaS厂商Salesforce旗下PaaS公司Heroku,任首席Ruby架构师,致力于加快RubyCore的开发。他还是NaCI及乐天技术研究所的研究员。著有Ruby in a NutshellThe RubyProgramming Language等书。他的博客地址为http://www.rubyist.net/~matz/

Ruby 是起源于日本的编程语言。近年来,特别是因为其在 Web 开发方面的效率很高,Ruby 引起了全世界的关注,它的应用范围也扩展到了很多企业领域。

作为一门编程语言,Ruby 正在被越来越多的人所了解,而作为一介工程师的我,松本行弘,刚开始的时候并没有想过“让全世界的人都来用它”或者“这下子可以大赚一笔了”,一个仅仅是从兴趣开始的项目却在不知不觉中发展成了如今的样子。

当然了,那时开发 Ruby 并不是我的本职工作,纯属个人兴趣,我是把它作为一个自由软件来开发的。但是世事弄人,现在开发 Ruby 竟然变成我的本职工作了,想想也有些不可思议。

“你为什么开发 Ruby?”每当有人这样问我的时候,我认为最合适的回答应该就像 Linux 的开发者 Linus Torvalds 对“为什么开发 Linux”的回答一样吧——

“因为它给我带来了快乐。”

当我还是一个高中生,刚刚开始学习编程的时候,不知何故,就对编程语言产生了兴趣。

周围很多喜欢计算机的人1,有的是“想开发游戏”,有的是“想用它来做计算”,等等,都是“想用计算机来做些什么”。而我呢,则想弄明白“要用什么编程语言来开发”、“用什么语言开发更快乐”。

1当时喜欢计算机的人当然还是少数。

高中的时候,我自己并不具备开发一种编程语言所必需的技术和知识,而且当时也没有计算机。但是,我看了很多编程语言类的书籍和杂志,知道了“还有像 Lisp 这样优秀的编程语言”、“Smalltalk 是做面向对象设计的”,等等,在这些方面我很着迷。上大学时就自然而然地选修了计算机语言专业。10 年后,我通过开发 Ruby 实现了自己的梦想。

从 1993 年开始开发 Ruby 到现在已经过去 16 年了。在这么久的时间里,我从未因为设计 Ruby 而感到厌烦。开发编程语言真是一件非常有意思的事情。

编程语言的重要性

为什么会这么喜欢编程语言?我自己也说不清。至少,我知道编程语言是非常重要的。

最根本的理由是:语言体现了人类思考的本质。在地球上,没有任何超越人类智慧的生物,也只有人类能够使用语言。所以,正是因为语言,才造成了人类和别的生物的区别;正是因为语言,人和人之间才能传递知识和交流思想,才能做深入的思考。如果没有了语言,人类和别的动物也就不会有太大的区别了。

在语言学领域里,有一个 Sapir-Whirf 假说,认为语言可以影响说话者的思想。也就是说,语言的不同,造成了思想的不同。人类的自然语言是不是像这个假说一样,我不是很清楚2,但是我觉得计算机语言很符合这个假说。也就是说,程序员由于使用的编程语言不同,他的思考方法和编写出来的代码都会受到编程语言的很大影响。

2在语言学中,Sapir-Whirf 假说好像是越来越站不住脚了。

也可以这么说,如果我们选择了好的编程语言,那么成为好程序员的可能性就会大很多。

20 年来一直被奉为名著的《人月神话》的作者 Frederick P. Brooks 说过:一个程序员,不管他使用什么编程语言,他在一定时间里编写的程序行数是一定的。如果真是这样,一个程序员一天可以写 500 行程序,那么不论他用汇编、C,还是 Ruby,他一天都应该可以写 500 行程序。

但是,汇编的 500 行程序和 Ruby 的 500 行程序所能做的事情是有天壤之别的。程序员根据所选择编程语言的不同,他的开发效率就会有十倍、百倍甚至上千倍的差别。

由于价格降低、性能提高,计算机已经很普及了。现在基本上各个领域都使用了计算机,但如果没有软件,那么计算机这个盒子恐怕一点用都没有了。而软件开发,就要求能够用更少的成本、更短的时间,开发出更多的软件。

需要开发的软件越来越多,开发成本却有限,所以对于开发效率的要求就很高。编程语言就成了解决这个矛盾的重要工具。

Ruby的原则

Ruby 本来是我因兴趣而开发的。因为对多种编程语言都很感兴趣,我广泛对比了各种编程语言,哪些特性好,哪些特性没什么用,等等,通过一一进行比较、选择,最终把一些好的特性吸纳进了 Ruby 编程语言之中。

如果什么特性都不假思索地吸纳,那么这种编程语言只会变成以往编程语言的翻版,从而失去了它作为一种新编程语言的存在价值。

编程语言的设计是很困难的,需要仔细斟酌。值得高兴的是,Ruby 的设计很成功,很多人都对 Ruby 给出了很好的评价。

那么,Ruby 编程语言的设计原则是什么呢?

Ruby 编程语言的设计目标是,让作为语言设计者的我能够轻松编程,进而提高开发效率。

根据这个目标,我制定了以下 3 个设计原则。

  • 简洁性

  • 扩展性

  • 稳定性

关于这些原则,下面分别加以说明。

简洁性

以 Lisp 编程语言为基础而开发的商业软件 Viaweb 被 Yahoo 收购后,Viaweb 的作者 Paul Graham 也成了大富豪。最近他又成了知名的技术专栏作家,写了一篇文章就叫“简洁就是力量”3

3Paul Graham 目前是世界知名的天使投资人,其公司 Y Combinator 投资了很多极有前途的创业项目。Paul Graham 曾出版过两本 Lisp 专著,最新著作《黑客与画家》已经由人民邮电出版社出版。——编者注

他还撰写了很多倡导 Lisp 编程语言的文章。在这些文章中他提到,编程语言在这半个世纪以来是向着简洁化的方向发展的,从程序的简洁程度就可以看出一门编程语言本身的能力。上面提到的 Brooks 也持同样的观点。

随着编程语言的演进,程序员已经可以更简单、更抽象地编程了,这是很大的进步。另外随着计算机性能的提高,以前在编程语言里实现不了的功能,现在也可以做到了。

面向对象编程就是这样的例子。面向对象的思想只是把数据和方法看作一个整体,当作对象来处理,并没有解决以前解决不了的问题。

用面向对象记述的算法也一定可以用非面向对象的方法来实现。而且,面向对象的方法并没有实现任何新的东西,却要在运行时判定要调用的方法,倾向于增大程序的运行开销。即使是实现同样的算法,面向对象的程序往往更慢,过去计算机的执行速度不够快,很难允许像这样的“浪费”。

而现在,由于计算机性能大大提高,只要可以提高软件开发效率,浪费一些计算机资源也无所谓了。

再举一些例子。比如内存管理,不用的内存现在可用垃圾收集器自动释放,而不用程序员自己去释放了。变量和表达式的类型检查,在执行时已经可以自动检查,而不用在编译时检查了。

我们看一个关于斐波那契(Fibonacci)数的例子。图1所示为用 Java 程序来计算斐波那契数。算法有很多种,我们用最常用的递归算法来实现。

class Sample {
  private static int fib (int n) {
     if (n<2) {
        return n;
     }
     else{
        return fib (n-2) +fib (n-1);
     }
  }
  public static void main (String[] argv) {
    System.out.println("fib(6)="+fib(6));
  }
}

图1 计算斐波那契数的 Java 程序

图2 所示为完全一样的实现方法,它是用 Ruby 编程语言写的,算法完全一样。和 Java 程 序相比,可以看到构造完全一样,但是程序更简洁。Ruby 不进行明确的数据类型定义,不必要的声明都可以省略。所以,程序就非常简洁了。

def fib (n)
  if n<2
    n
  else
    fib (n-2) +fib (n-l)
  end
end
print "fib (6) =", fib (6) , "\n"

图2 计算斐波那契数的 Ruby 程序

算法的教科书总是用伪码来描述算法。如果像这样用实际的编程语言来描述算法,那么像类型定义这样的非实质代码就会占很多行,让人不能专心于算法。

如果可以把伪码中非实质的东西去掉,只保留描述算法的部分就直接运行,那么这种编程语言不就是最好的吗?Ruby 的目标就是成为开发效率高、“能直接运行的伪码式编程语言”。

扩展性

下一个设计原则是“扩展性”。编程语言作为软件开发工具,其最大的特征就是对要实现的功能事先没有限制。“如果想做就可以做到”,这听起来像小孩子说的话,但在编程语言的世界里,真的就是这么一回事。不管在什么领域,做什么处理,只要用一种编程语言编写出了程序,我们就可以说这种编程语言适用于这一领域。而且,涉及领域之广会远远超出我们当初的预想。

1999 年,关于 Ruby 的第一本书《面向对象脚本语言 Ruby》出版的时候,我在里面写道,“Ruby 不适合的领域”包括“以数值计算为主的程序”和“数万行的大型程序”。

但是几年后,规模达几万行、几十万行的 Ruby 程序被开发出来了。气象数据分析,乃至生物领域中也用到了 Ruby。现在,美国国家海洋和大气管理局(NOAA,National Oceanic and Atmospheric Administration)、美国国家航空和航天局(NASA,National Aeronautics and Space Administration)也在不同的系统中运用了 Ruby。

情况就是这样,编程语言开发者事先并不知道这种编程语言会用来开发什么,会在哪些领域中应用。所以,编程语言的扩展性非常重要。

实现扩展性的一个重要方法是抽象化。抽象化是指把数据和要做的处理都封装起来,就像一个黑盒子,我们不知道它的内部是怎么实现的,但是可以用它。

以前的编程语言在抽象化方面是很弱的,要做什么处理首先要了解很多编程语言的细节。而很多面向对象或者函数式的现代编程语言,都在抽象化方面做得很好。

Ruby 也不例外。Ruby 从刚开始设计时就用了面向对象的设计方法,数据和处理的抽象化提高了它的开发效率。我在 1993 年设计 Ruby 时,在脚本编程语言中采用面向对象思想的还很少,用类库方式来提供编程语言的就更少了。所以现在 Ruby 的成功,说明当时采用面向对象方法的判断是正确的。

Ruby 的扩展性不仅仅体现在这些方面。

比如 Ruby 以程序块这种明白易懂的形式给程序员提供了相当于 Lisp 高阶函数的特性,使“普通的程序员”也能够通过自定义来实现控制结构的高阶函数扩展。又比如已有类的扩展特性,虽然有一定的危险性,但是程序却可以非常灵活地扩展。关于这些面向对象、程序块、类扩展特性的内容。

这些特性的共同点是,它们都表明了编程语言让程序员最大限度地获得了扩展能力。编程语言不是从安全性角度考虑以减少程序员犯错误,而是在程序员自己负责的前提下为他提供最大限度发挥能力的灵活性。我作为 Ruby 的设计者,也是 Ruby 的最初用户,从这种设计的结果可以看出,Ruby 看重的不是明哲保身,而是如何最大限度地发挥程序员自身的能力。

关于扩展性,有一点是不能忽视的,即“不要因为想当然而加入无谓的限制”。比如说,刚开始开发 Unicode 时,开发者想当然地认为 16 位(65 535 个字符)就足够容纳世界上所有的文字了;同样,Y2K 问题也是因为想当然地认为用 2 位数表示日期就够了才导致的。从某种角度说,编程的历史就是因为想当然而失败的历史。而 Ruby 对整数范围不做任何限定,尽最大努力排除“想当然”。

稳定性

虽然 Ruby 非常重视扩展性,但是有一个特性,尽管明知道它能带来巨大的扩展性,我却一直将其拒之门外。那就是宏,特别是 Lisp 风格的宏。

宏可以替换掉原有的程序,给原有的程序加入新的功能。如果有了宏,不管是控制结构,还是赋值,都可以随心所欲地进行扩展。事实上,Lisp 编程语言提供的控制结构很大一部分都是用宏来定义的。

所谓 Lisp 流,其语言核心部分仅仅提供极为有限的特性和构造,其余的控制结构都是在编译时通过用宏来组装其核心特性来实现的。这也就意味着,由于有了这种无与伦比的扩展性,只要掌握了 Lisp 基本语法 S 式(从本质上讲就是括号表达式),就可以开发出千奇百怪的语言。Common Lisp 的读取宏提供了在读取 S 式的同时进行语法变换的功能,这就在实际上摆脱了 S 式的束缚,任何语法的语言都可以用 Lisp 来实现。

那么,我为什么拒绝在 Ruby 中引入 Lisp 那样的宏呢?这是因为,如果在编程语言中引入宏的话,活用宏的程序就会像是用完全不同的专用编程语言写出来的一样。比如说 Lisp 就经常有这样的现象,活用宏编写的程序 A 和程序 B,只有很少一部分是共通的,从语法到词汇都各不相同,完全像是用不同的编程语言写的。

对程序员来说,程序的开发效率固然很重要,但是写出的程序是否具有很高的可读性也非常重要。从整体来看,程序员读程序的时间可能比写程序的时间还长。读程序包括为理解程序的功能去读,或者是为维护程序去读,或者是为调试程序去读。

编程语言的语法是解读程序的路标。也就是说,我们可以不用追究程序或库提供的类和方法的详细功能,但是,“这里调用了函数”、“这里有判断分支”等基本的“常识”在我们读程序时很重要。

可是一旦引入了宏定义,这一常识就不再适用了。看起来像是方法调用,而实际上可能是控制结构,也可能是赋值,也可能有非常严重的副作用,这就需要我们去查阅每个函数和方法的文档,解读程序就会变得相当困难。

当然了,我知道世界上有很多 Lisp 程序员并不受此之累,他们正是通过面向特定程序定制语言而最大限度地提高了开发效率。不过在我个人看来,他们只是极少数的一部分程序员。

我相信,作为在世界上广泛使用的编程语言,应该有稳定的语法,不能像随风飘荡的灯芯那样闪烁不定。

一切皆因兴趣

当然,Ruby 不是世界上唯一的编程语言,也不能说它是最好的编程语言。各种各样的编程语言可以在不同的领域中应用,各有所长。我自己以及其他 Ruby 程序员,用 Ruby 开发效率很高,所以觉得 Ruby“最为得心应手”。当然,用惯了 Python 或者 Lisp 的程序员,也会觉得那些编程语言是最好的。

不管怎么说,编程语言存在的目的是让人用它来开发程序,并且尽量能提高开发效率。这样的话,才能让人在开发中体会到编程的乐趣。

我在海外讲演的时候,和很多人交流过使用 Ruby 的感想,比较有代表性的是:“用 Ruby 开发很快乐,谢谢!”

是啊,程序开发本来就是一件很快乐、很刺激和很有创造性的事情。想起中学的时候,用功能不强的 BASIC 编程语言开发,当时也是很快乐的。当然,工作中会有很多的限制和困难,编程也并不都是一直快乐的,这也是世之常情。

Ruby 能够提供很高的开发效率,让我们在工作中摆脱很多困难和烦恼,这也是我开发 Ruby 的目的之一吧。

 

{%}

《松本行弘的程序世界》不是为了介绍某种特定的技术,而是从宏观的角度讨论与编程相关的各种技术。书中第1章介绍了作者对编程问题的新思考和新看法,剩下的内容出自《日经Linux》杂志于2005年5月到2009年4月连载的“松本编程模式讲坛”,其中真正涉及“模式”的内容并不多,大量篇幅都用于介绍技术内幕和背景分析等内容,使读者真正了解相关技术的立足点。另外,书中还包含许多以Ruby、Lisp、Smalltalk、Erlang、JavaScript等动态语言所写成的范例。本文节选自《松本行弘的程序世界》

Clojure哲学

{%}

作者/ Chris Houser

Chris Houser是Clojure的关键贡献者,曾实现了几个重要的特性。

通常说来,学习新语言要在智力和精力上都有极大的投入,只有程序员预期所学语言能够物有所值,这样的投入才是公平的。Clojure 出自 Rich Hickey 的手笔,他试图规避使用传统面向对象技术管理可变状态带来的诸多复杂性:既有本质的,也有偶然的。凭借对程序设计语言严肃研究而进行的贴心设计,以及对实用性的热切追求,Clojure 逐渐发展为一门重要的程序设计语言。它正扮演起一个不可忽视的重要角色,体现着程序语言设计的最新发展方向。在等式的一边,Clojure 充分利用了软件事务性内存(Software Transactional Memory,STM)、代理(agent)、标识和值类型之间的清晰划分、任意的多态以及函数式编程等诸多特性,总的来说为理清状态提供了一个有益的环境,尤其是在并发方面。另一方面,Clojure 同 Java 虚拟机是一种共生的关系,这样一来,程序员们可以利用既有的程序库,而不必维护另一套基础设施。

在程序设计语言的历史长河中,Clojure只是一个婴儿,但其用法(或是说“最佳实践”或惯用法)却源自有 50 年历史的Lisp,以及有 15 年历史的Java。此外,自问世以来,其热情的社区就呈现出爆炸式增长,培育出自己独特的一套惯用法。正如前言中提及,一种语言的惯用法让我们可以用简洁的形式表现更复杂的东西。虽然我们肯定会涵盖Clojure的惯用代码,但我们还想进一步探讨语言本身“为什么”要设计成这样。

虽然借鉴了 Lisp(总的来说)和 Java 的传统,Clojure 在很多方面的改变却代表着对它们直接面临的挑战。

在本文中,我们会讨论一些既有语言的缺陷,这也是 Clojure 设计要解决的,在这些领域里,Clojure 有着怎样的优势,以及 Clojure 包含的诸多设计决策。

Clojure 之道

我们会慢些起步。

Clojure 是一门观点鲜明的语言,它并不打算涵盖所有编程范式,也不准备提供清单列出每个重要特性。相反,它只提供以 Clojure 之道解决各种真实问题所需的特性。要从 Clojure 中获得最大收益,我们就要写出遵循语言自身愿景的代码。我们想说的并不只是一个特性做了些什么,更重要的是,为什么会有这样的特性,以及如何利用好这样的特性。

但是,开始之前,我们先来从宏观上了解一下 Clojure 最重要的哲学基础。图 1 列出了 Rich Hickey 设计 Clojure 时头脑中一些大致的目标,以及为了支持这些目标而内建在语言中的一些更具体的决策。

{%}

图1 Clojure 的大致目标:本图展示了构成 Clojure 哲学的一些概念,以及这些概念之间的交互

如图 所示,Clojure 的总目标由一些支持目标和功能综合而成。

简单

复杂问题很难有一个简单的解决方案。但是,如果把事情搞得不必要的复杂,即便是有经验的程序员也会栽倒,这就是“偶然复杂性”,与其相对的是任务的本质复杂性(Moseley 2006)。Clojure 致力于帮我们解决各种复杂问题,而不引入偶然复杂性,比如,各种数据需求、多并发线程、独立开发的程序库等。它还提供了一些工具,减少了一些初看起来像本质复杂性的东西。如此一来,最终的特性集合或许看起来并不简单,尤其在我们对这些特性还不甚熟悉时,但随着继续学习,我们认为,你会逐渐体会到 Clojure 去除了多少的复杂性。

偶然复杂性有一个例子,就是现代面向对象程序设计语言的一个发展趋势,即它要将所有可运行代码打包在类定义、继承和类型声明这样的层次里。Clojure 通过支持“纯函数”去除了所有这些东西,所谓纯函数就是传入几个实参,然后,只根据这些实参产生一个返回值。Clojure 很大一部分就是构建在这样的函数基础上的,绝大多数应用也可以如此,这意味着,尝试解决手头问题时,需要考虑的东西会更少。

专注

写代码总是要和干扰做斗争,每当语言让我们思考语法、运算符优先级、继承层次结构时,只会让干扰增多。Clojure 尽力让一切保持尽可能简单,无需为探索一个想法经历“编译—运行”的循环,无需类型声明,等等。它还提供了一些工具,让我们可以改造语言,使词汇和文法能够更好地适应问题领域,因此,Clojure 极具表现力。这种做法影响极大,可以在不牺牲可理解性的前提下,很好地完成一些极其复杂的任务。

之所以能够保持专注,关键一点在于恪守对动态系统的承诺。Clojure 程序中定义的几乎所有一切都是可以重新定义的,即便程序尚在运行:函数、多重方法、类型、类型层次结构,甚至 Java 的方法实现。动态重定义这些东西貌似很可怕,尤其是在生产系统上,但它却为思考如何编写程序打开了另一种奇妙的可能性。我们可以对不熟悉的 API 进行更多的实验和探索,这是一种乐趣,而这种乐趣却常常为更静态的语言、漫长的编译周期所阻碍。

但是,Clojure 并不只有乐趣。乐趣只是一种副产品,更重要的是,它可以让程序员有能力获得超乎想象的高效。

实用

某些程序设计语言生来只为展示学术成果,或是探索某种计算理论。Clojure 不在此列。Rich Hickey 曾在很多场合说过,在构建有趣且有用的应用方面,Clojure 是很有价值的。

为达此目标,Clojure 努力做到务实 —— 一种用于完成工作的工具。在 Clojure 里,如果某一设计决策要在实用和聪明、花哨或是纯理论的解决方案进行权衡,胜者往往是那些实用的解决方案。Clojure 曾试图让我们远离 Java,但这样做要在程序员和程序库之间插入大量 API,这种做法可能会让第三方 Java 程序库很难用。所以,Clojure 选择了另一条路:不做封装、编译成相同的字节码,能够直接访问 Java 的类和方法。Clojure 字符串就是 Java 字符串;Clojure 函数调用就是 Java 方法调用。这样做简单、直接、务实。

使用Java虚拟机(JVM)这个决策本身就是一个务实的做法。JVM存在某些技术上的缺陷,诸如启动时间、内存使用、缺乏尾递归优化(tail-call optimization,TCO)。但是,它也是一个惊人的务实平台——成熟、快速、部署广泛。其支持各种硬件和操作系统,拥有数量众多的程序库,以及支持工具,由于这个极尽务实的决策,所有这一切都可以为Clojure所用。

如果你不了解尾递归优化是什么,请不必担心。如果你知道 TCO 是什么,也不必担心 JVM 在这方面的欠缺会成为 Lisp 或是像 Clojure 这样函数式语言的致命缺陷。

除了直接的方法调用外,Clojure 还有 proxy、gen-class、gen-interface、reify、definterface、deftype 和 defrecord,为互操作性提供了许多选择,所有这些都是为了完成工作。务实对 Clojure 很重要,当然,许多其他语言也同样务实。我们后面会看到 Clojure 摆脱混乱的一些做法,正是这些地方让它显得与众不同。

清晰

When beetles battle beetles in a puddle paddle battle and the beetle battle puddle is a puddle in a bottle they call this a tweetle beetle bottle puddle paddle battle muddle.

—Dr. Seuss

下面有一段简单的代码,可能是用 Python 写的:

x = [5]
process(x)
x[0] = x[0] + 1

执行这段代码之后,x 的值是什么呢?如果假设 process 没有改变 x 的内容,那就应该是[6],对吧?但是,怎样才能做这样的假设呢?如果不了解 process 做了些什么,调用了怎样的函数等,我们根本无法确认。

就算 process 不会改变 x 的值,这时,再加入多线程,我们还是会有一大堆顾虑。如果在第一行和第三行之间,另一个线程改变了 x 会怎么样?还有更糟糕的,如果在第三行做赋值时,某个东西设置了 x,那又该如何?你能保证你的平台写变量是原子操作吗?或者,是不是最终的值可能是多个写操作混杂的结果?我们可以抱着获得某种清晰的想法,将这个思维训练无休止地进行下去,但结果是一样的——我们根本无法得到清晰,只会适得其反:混乱。

Clojure 为代码的清晰做着努力,提供了一些工具规避几种不同的混乱。就刚才描述的那种情况而言,采用它所提供的不变局部量和持久化集合,便可一并消除了单线程和多线程的大部分问题。

当我们所用的语言将不相关的行为合在一个构造里时,我们不难发现,自己已深陷多种泥潭。Clojure 通过分离关注点让我们保持警醒,应对这样的情况。一旦事物得到分离,思路就会清晰许多,只在必要时重新组合。从某种程度上说,这样的做法对某些特定问题非常有用。

有时,很难在脑子里将这些概念区分开来,但如果能做到的话,就会非常清晰了,为了这种强大和灵活,我们值得努力一试。我们有那么多不同的概念要处理,以一致的方式表现代码和数据就显得很重要了。

一致

Clojure 在两个具体的方面提供了一致性:语法和数据结构。

语法一致性指的是,相关的概念在形式上是类似的。有个简洁有力的例子,for 和 doseq 这两个宏之间的语法是一样的。

它们做的事情不尽相同——for 返回的是一个惰性 seq,而 doseq 只是为了产生副作用——但二者支持相同的迷你语言(mini-language):嵌套迭代、解构、:when 和:while 卫语句。比较下面这个例子就不难看出相似性:

(for [x [:a :b], y (range 5) :when (odd? y)] [x y])
;=> ([:a 1] [:a 3] [:b 1] [:b 3])

(doseq [x [:a :b], y (range 5) :when (odd? y)] (prn x y))
; :a 1
; :a 3
; :b 1
; :b 3
;=> nil

这种相似的价值在于,只要学习一种基本语法即可应对两种情况,必要时,在两种用法间切换也会容易许多。

类似地,数据结构的一致性表现在 Clojure 持久化集合类型的精心设计上,它为各个类型提供了尽可能相似的接口,并尽可能广泛地去使用这些接口。这种做法实际上是 Lisp 经典的“代码即数据”哲学的扩展。Clojure 数据结构不只可以持有一大堆应用的数据,还可以持有应用自身的一些表达式元素。它们可以描述对 form 的解构,还可以为各种内建函数提供命名选项(named options)。其他面向对象语言可能会鼓励应用定义多个彼此不相容的类,以持有不同类型的应用数据,而 Clojure 则鼓励使用行为上类似于 map 的对象。

这样做的好处在于,同样一套处理 Clojure 数据结构的函数可以用于下列所有情形:大规模数据存储、应用代码和应用数据对象。用 into 可以构建任意类型,用 seq 可以获取一个用于遍历的惰性 seq,用 filter 可以选择满足特定条件的元素,等等。一旦习惯了所有这些丰富且随处可用的函数,用 Java 或 C++处理应用中的 Person 或 Address 这样的类就会让人觉得处处掣肘。

简单、专注、实用、一致和清晰。

Clojure 程序设计语言里几乎所有元素都是为了提振这些目标。编写 Clojure 代码处理真实问题时,请将“简单、实用、专注”等方面推向极致,将此铭记于心,我们相信你会发现,Clojure 就是你到达成功彼岸所需的工具。

为何(又一种)Lisp

一套好的概念可以将大脑从无用功中解放出来,专注于更高级的问题。

——Alfred North Whitehead

去到任何一个开源项目托管站点,搜索“Lisp interpreter”(Lisp解析器)。这个貌似平淡无奇的搜索,可能会带给我们一个堆积如山 4的结果。事实上,计算机科学的历史中堆积着大量废弃的Lisp实现(Fogus 2009)。诸多初衷良好的Lisp来了又走,一路遭到无数嘲笑,但到了明天,搜索结果依然会无限制增长。既然有如此惨痛的过往,为何还有人愿意将其崭新的程序设计语言构建于Lisp模型之上呢?

4……且疯狂的。

优美

Lisp 吸引了计算机科学史上最聪明的一群头脑。但是,仅有权威的争论是不够的,我们不该仅凭此判断 Lisp。只有用其编写应用,才可以直接看得到 Lisp 语言家族的真正价值。Lisp 的风格就在于极具表现力、非常实用,以及在大多数情况下表现出的美感。最初的 Lisp 语言是 John McCarthy 在其惊天动地的论文“Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”(McCarthy 1960)中定义的,只用区区 7 个函数和两个特殊 form 便定义出整个语言:atom、car、cdr、cond、cons、eq、quote、lambda 和 label。

通过这 9 个 form 的组合,McCarthy 将整个计算以一种令人窒息的方式呈现出来。计算机程序员总在寻找美,而多数情况下,美会以简单的形式自我呈现出来。7 个函数 2 个特殊 form,美无过于此。

极度灵活

Lisp 何以历经五十余年而弥新,相较之下,无数语言却成了匆匆过客?个中原因可能极尽复杂,但究其根因,无外乎 Lisp 自身的语言基因(Tarver 2008)将语言的灵活性推向极致。Lisp 新手常常气馁,无处不在的括号和前缀记法,与非 Lisp 程序设计语言大相径庭。然而,正是这种行为上的规律性,不仅让需要记忆的语法规则减少了,也让宏的编写变得很简单。下面是一个例子,稍后再来细究:

(defn query [max]
  (SELECT [a b c]
    (FROM X
      (LEFT-JOIN Y :ON (= X.a Y.b)))
    (WHERE (AND (< a 5) (< b ~max)))))

这里要说的是,Clojure 并没有内建对 SQL 的支持。SELECT、FROM 等这些词并不是内建的 form。它们也不是常规的函数,如果 SELECT 是,那么使用 a、b 和 c 就错了,因为它们还没有定义。

如何用 Clojure 定义这样的领域特定语言(domain-specific language,DSL)呢?好吧,这不是产品就绪(production-ready)的代码,没有绑定到真实的数据库服务器上;但只要有了程序 1.1 列出的一个宏和三个函数,前面的查询就能够返回下面这些实际的值:

(query 5)
;=> ["SELECT a, b, c FROM X LEFT JOIN Y ON (X.a = Y.b)
      WHERE ((a < 5) AND (b < ?))"
     [5]]

请注意,FROM 和 ON 这样的词是从输入表达式中直接取出来的,而其他诸如~max 和 AND 则要特殊对待。调用查询时,max 得到一个 5,这是从字面量 SQL 字符串中提取的,由一个单独向量提供,以这种方式准备的查询颇为完美,可以免受 SQL 注入攻击。AND form 由 Clojure 的前缀表达式转成 SQL 所需的中缀表达式。

程序1 以 Clojure 编写领域特定语言,用以嵌入 SQL 查询

(ns joy.sql
  (:use [clojure.string :as str :only []])        ; 使用核心的 string 函数

(defn expand-expr [expr]
  (if (coll? expr)
    (if (= (first expr) `unquote)          ; 处理不安全的字面量
      "?"
      (let [[op & args] expr]
        (str "(" (str/join (str " " op " ")
                           (map expand-expr args)) ")")))
    expr))                                                        ; 将前缀转为中缀

(declare expand-clause)

(def clause-map                                    ; 支持各种语句
  {'SELECT    (fn [fields & clauses]
                (apply str "SELECT " (str/join ", " fields)
                       (map expand-clause clauses)))
   'FROM      (fn [table & joins]
                (apply str " FROM " table
                       (map expand-clause joins)))
   'LEFT-JOIN (fn [table on expr]
                (str " LEFT JOIN " table
                     " ON " (expand-expr expr)))
   'WHERE     (fn [expr]
                (str " WHERE " (expand-expr expr)))})

(defn expand-clause [[op & args]]                      ; 调用适当的转换器
  (apply (clause-map op) args))
(defmacro SELECT [& args]                          ; 提供主入口宏
  [(expand-clause (cons 'SELECT args))
   (vec (for [n (tree-seq coll? seq args)
              :when (and (coll? n) (= (first n) `unquote))]
          (second n)))])

但需要指出的是,这算不上是一种很好的SQL DSL——还有实现更为完整的。我 们要说的是,一旦懂得了这种创建DSL的技巧,就可以识别出一些机会,定义自己的DSL,解决比SQL更窄的、更加面向应用的问题。无论是给不常见的非SQL数据库提供查询语言,还是给模糊的数学学科提供一种方式表现函数,抑或是处理其他自己都未曾想过的应用,能够拥有如此灵活易扩展的基础语言,且不损伤语言自身特性,都将成为游戏规则的改变者。

要提到的一个是 ClojureQL,它位于 http://gitorious.org/clojureql

虽然我们不该太过深入细节地讨论实现,但还是要顺着之前讨论过的一些重要方面,简单看看列表 1 的实现。

自下而上阅读,首先映入眼帘的是入口点,SELECT 宏。它返回的是一个有两项的 vector——第一项通过调用 expand-clause 生成,返回的是一个经过转换的查询字符串,而第二项是另一个 vector,表示输入里由~标记的表达式。~表示反 quote。另外要注意的是这里用到的 tree-seq,通过它可以很容易地将感兴趣的项从值树(也就是输入表达式)上提取出来。

expand-clause 函数用语句的第一个词,在 clause-map 里进行了查询,然后,调用适当的函数,完成从 Clojure 的 s 表达式(s-expression)到 SQL 字符串的转换。clause-map 为 SQL 表达式各个部分提供了所需的详细功能:插入逗号或是其他 SQL 语法,有时还要递归调用 expand-clause 进行子语句的转换。其中之一是 WHERE 语句,通过委托给 expand-expr 函数,处理了 SQL 所需的前缀表达式到中缀表达式的通用转换。

总的来说,这个例子展示的 Clojure 灵活性大多是因为宏可以接受代码 form(比如前面展示的这个 SQL DSL 的例子),并将其当做数据对待——遍历树、转换值等。之所以可以这样做,不只是因为代码可以当做数据,还因为在 Clojure 程序里,代码就是数据。

代码即数据

“代码即数据”这样的说法最初很难理解。实现一门程序设计语言,代码同数据一般对待,这需要语言本身具有非常强的可塑性。当语言就是以这种本质的数据结构表现时,语言本身就可以操作自己的结构和行为了(Graham 1995)。读到上面这句话,我们脑海中可能会浮现出一条衔尾蛇(Ouroboros),也许这么说不合适,因为Lisp可以比作一个自我舔食的棒棒糖——更正规的说法是同像性(homoiconicity)。要完全掌握Lisp的同像性,需要跨越一个巨大的概念鸿沟,我们会尽力帮你理解这个概念,希望你最终能够领会其巨大的威力。

衔尾蛇(Ouroboros)是自古流传至今的一种符号,大致形象是一条蛇正吞食自己的尾巴,结果形成了一个圆环。更多信息可以参考:http://zh.wikipedia.org/wiki/衔尾蛇

初学 Lisp 是一番乐趣,如果你能通过学习得到同样的体验,那么我们欢迎你——甚至有点嫉妒。

函数式编程

快点回答,函数式编程是什么意思?错!

别太泄气,其实,我们也不知道确切的答案是什么。函数式编程只是诸多定义模糊的计算机术语7中的一个。如果找 100 个程序员问它的定义,我们会得到 100 个不同的答案。确实,某些答案是类似的,但如同雪花一般,没有两个答案是完全一样的。要进一步搅混水的话,让计算机科学的专家们单独给出定义,我们可能会发现,某些答案甚至是彼此矛盾的。同样,任何一个函数式编程定义的基本结构都可能会不同,这完全取决于回答问题的人喜欢用哪种语言写程序:Haskell、ML、Factor、Unlambda、Ruby、Qi等。随便一个人、一本书或是一门语言怎么就能声称自己是函数式编程的权威呢?然而,正如大多数各具特色的雪花都是由水组成的,各种说法的函数式编程核心都遵循着同样的核心原则。

7快点回答,组合子(combinator)的定义是什么?云计算呢?企业级呢?SOA 呢?Web 2.0 呢?真实世界呢?黑客呢?通常,追求有唯一准确定义这件事无异于缘木求鱼。

一个可行的函数式编程定义

无论函数式编程定义用的是 lambda 演算、单子 I/O(monadic I/O)、delegate 还是 java.lang. Runnable,基本的单元可能就是某种形式的过程、函数或是方法——这是根本。函数式编程关心和处理的是函数的应用和组合。再进一步,一门被认为是函数式的语言,它的函数概念一定是一等的。在这门语言里,函数可以存储、可以传递,还可以返回,同语言里的其他数据一样。各种不同的定义远远超出了这一核心概念,但是,谢天谢地,作为起点,这足够了。当然,我们还会进一步阐述一下 Clojure 风格的函数式编程,包括纯粹性、不变性、递归、惰性和引用透明等主题。

函数式编程的内涵

一般说来,面向对象程序员和函数式程序员看到问题和解决问题的方式有所不同。面向对象思维模式采用的方式是,把应用领域定义成一组名词(类),函数式思维模式则会把解决方案视为各种动词及其组合(函数)。虽然二者产生的结果可能是一样的,但函数式解决方案会在简洁、可理解、可重用方面更胜一筹。确实如此!函数式编程会让程序设计更为优雅。这是一种思维模式的转换,从考虑名词,到思考动词,但这个旅程物有所值。无论如何,我们都相信,Clojure 会让你获益良多,反哺到你选择的语言中——唯有打开心胸,方能体会这一点。

Clojure 为何不是面向对象的

优雅同熟悉正交。

——Rich Hickey

Clojure 的出现源自一种无奈,很大程度要归因于并发编程复杂性以及面向对象程序设计在这方面的无能为力。接下来我们将会探索这些缺陷,了解 Clojure 之所以是函数式而非面向对象的根因。

定义术语

开始之前,先定义术语。

这些术语是 Rich Hickey 在其演讲“Are We There Yet?”(Hickey 2009)里定义和详细阐述的。

第一个要定义的重要术语是时间(time)。简单说来,时间是指事件发生的相对时刻。有了时间,同实体关联在一起的属性——无论是静态还是动态,单数的还是组合的——会形成一种共生关系(Whitehead 1929),从逻辑上说,可以认为是其标识(identity)。在任意给定的时间,都可以得到实体属性的快照,这个快照定义了状态(state)。在这种概念里,状态是不可变的,因为状态没有在实体本身内定义成一种可变的东西,只是展现某个给定时刻的属性而已。想象一下,有一本儿童手翻书,如图 2 所示,完全是为了理解这些术语。

{%}

图2 奔跑者:儿童手翻书,用以解释 Clojure 状态、时间和标识的概念。书本身表示标识。当我们希望插图有所改变时,就画另一幅图,加到手翻书的末尾。翻动书页的动作,表示状态随时间改变。停到给定页面,观察特定图片,表示某一时刻奔跑者的状态

有一件事很重要,要特别提一下,在面向对象程序设计的加农炮里,状态和标识并没有清晰的区分。换句话说,这两个概念合并成一个通常称为可变状态的东西。经典的面向对象模型对对象属性的修改毫无限制,完全不会保留历史状态。Clojure 的实现尝试在对象状态和标识(因为其与时间有关)之间画出一条清晰的界限。同样是上面手翻书的例子,采用可变状态模型结果是完全不同的,为了表示与 Clojure 模型之间的差异,可以参考图3所示。

{%}

图3 可变的奔跑者:将状态改变建模为可变的,需要准备一些橡皮擦。书只有一页,状态改变时,我们必须物理擦除,根据修改重绘图片的一部分。采用这样的模型可以看出,可变性摧毁了时间、状态和标识这些概念,变成了只有一个

不变性是Clojure的基石,Clojure实现的绝大部分都是为了高效地支持不变性。通过关注不变性,Clojure完全消除了可变状态(这是一个矛盾修辞法的概念),这说明大多数对象表示的东西其实都是值。从定义上说,值是指对象固定不变的代表值 、量级或是时间段等。或许,你会问自己:在Clojure里,这种基于值的编程语义内涵到底是什么呢?

之所以说可变状态是一个矛盾修辞法,是因为在 Clojure 里,状态就是一个关于不变性的术语。

某些实体没有代表值——Pi 就是其中之一。但在计算领域,最终处理的都是有限的事物,这是个悬而未决的问题。

很自然,遵循严格的不变性模型,并发一下子就变成一个比较简单(虽然还是不那么简单)的问题,这意味着,如果不必顾忌对象状态的改变,我们便可肆无忌惮地共享,而无惧并发修改。Clojure 把值的修改与其引用类型隔离开来。Clojure 的引用类型为标识提供了一个间接层,这样一来,标识就可以用于获得一致的状态,如果不总是当前状态的话。

命令式“烘烤”

命令式编程是如今占主导地位的编程范式。命令式程序设计语言最纯粹的定义是,这种语言用一系列语句修改程序状态。这样的事实本质上没那么糟糕,因为无数成功的软件项目就是用面向对象命令式编程技术构建的。但在并发编程的上下文里,面向对象命令式模型却是自我吞食 的。命令式模型允许(甚至鼓励)无限制地修改变量,所以,它并不直接支持并发。如果对修改的不加控制,那么任何变量都无法保证包含的值是符合预期的。面向对象程序设计将状态聚合在对象内部,朝着这个方向又迈了一步。虽然加锁机制让单个方法可能是线程安全的,但是,如果不采用更为复杂的加锁机制,并扩大加锁范围,就没有办法在多个方法调用间保证对象状态的一致性。而Clojure则关注于函数式编程、不变性,注意区分状态、时间和标识。当然,面向对象并没有彻底失去希望。实际上,它在很多方面还是可以促进编程实践的。

关于自我吞食,参见前面“衔尾蛇”的注解。

并非万物皆对象

最后要说一点,面向对象程序设计的另一个不足之处是,函数和数据之间绑定过紧。事实上,Java程序设计语言强迫我们把整个程序完全构建在类层次结构上,所有功能都必须出现在高度受限的“名词王国”(Yegge 2006)所包含的方法中。这一环境如此受限,以致于程序员们只能被迫闭上双眼,否则将无法面对这些组织不当的方法和类带来的尴尬结果。正是因为这种极尽苛刻的以对象为中心的视角,导致了Java代码显得嗦而复杂(Budd 1995)。Clojure的函数就是数据,然而,对于数据及处理数据的函数而言,并不要求一定要将二者解耦。许多程序员认为是类的东西,实际上就是Clojure用map和记录形式提供的数据表。对“视万物为对象”的最后一击是,在数学家眼里,没有什么东西是对象(Abadi 1996)。相反,数学通过应用函数,构建于一组元素同另一组元素之间的关系基础之上。

 

{%}

《Clojure编程乐趣》并非Clojure初学指南,也不是一本Clojure的编程操作手册,而是通过对Clojure详尽地探究,教授函数式的程序设计方式,帮助读者理解和体会Clojure编程的乐趣,进而开发出优美的软件。本文节选自《Clojure编程乐趣》

掌控Web的语言JavaScript

{%}

作者/ 井上诚一郎

井上诚一郎曾在美国参与过Lotus Notes的开发,后在日本创立了Ariel Network股份公司,任CTO。目前从事面向企业的PSP软件及企业产品的开发。著有《PSP教科书》、《Java编程详解》、《实践JS 服务器端JavaScript入门》等书。

本文将介绍 JavaScript 和 ECMAScript 的关系与历史,以及 JavaScript 与作为其实现方式和运行环境的浏览器的关系,此外还将总括 JavaScript 的可移植性。

JavaScript 概要

我们首先介绍 JavaScript 相关的运行环境。你应该知道 JavaScript 是在浏览器中运行的语言吧。甚至可以说,除开发者以外,被大众所熟知的程序设计语言也许只有 JavaScript。而且在软件史上,以能够在各种环境下运行而著称的语言中,大概没有比 JavaScript 更有名的了。

但是,正是由于太过常见,才让很多人对 JavaScript 有了一些误解与偏见。

例如,因为和浏览器的关联性过强,很多人都以为 JavaScript 只能在浏览器中运行。对 JavaScript 的看法也是莫衷一是。有人认为它降低了 Web 的使用体验,也有人称赞它是一门使 Web 的易用性得以进化的出色的技术。有人觉得 JavaScript 是任何人都可以学会的简单语言,也有人认为它过于抽象,很难掌握。

对 JavaScript 的看法各有不同,很难说哪一种正确。不过,只要软件以 Web 为中心,今后 JavaScript 的重要性就一定会进一步提升。JavaScript 领域的名人道格拉斯 • 克罗克福德曾把 JavaScript 称为 Web 上 的虚拟机。其核心含义是,在 JavaScript 广为普及的现在,Web 已经成为了 JavaScript 事实上的运行环境。夸张地讲,JavaScript 正日益成为支配世界的程序设计语言。

虽说 JavaScript 已被逐渐应用于浏览器之外的场合,但就目前而言,其主战场还是浏览器。

JavaScript 的历史

JavaScript 于 1995 年登场,运用在当时最流行的浏览器 Netscape Navigator 中。在此之前,浏览器只能处理 HTML 与图片,而 JavaScript 使得浏览器端的程序运行成为可能。

能够在浏览器中运行程序,并非 JavaScript 的专利。其先驱是另一门著名的程序设计语言 Java,主要用于服务器端。当初被称为 Java Applet 的程序由于可以在浏览器(HotJava)中运行而广受瞩目。

众所周知,尽管 Java 和 JavaScript 在保留字和关键字等表层范畴上很相似,但作为程序设计语言,它们之间其实并没有什么关系。JavaScript 开发得较晚,开发之初的名称是 LiveScript,之后才决定效仿已经颇为有名的 Java,改为 JavaScript。虽然 Java 和 JavaScript 的命名导致了许多误解,但回顾历史,可以说这是一种正确的营销手段。

稍微了解一下语言规则就会发现,Java 和 JavaScript 的执行方式并不像其表面那样相似。JavaScript 反而和 Ruby 或 Python 这样的轻型脚本语言,或 Lisp 之类的以函数作为主体的程序设计语言更为相似。不过由于早期主要是跟随 Java 发展,因此 JavaScript 的对象名以及方法名和 Java 比较相似。

JavaScript 简史

在此,我们总结一下 JavaScript 标准的制定时间和一些重要事件(表1)。ECMAScript 将在后面中进行说明。

表1 JavaScript简史

1995年

网景公司开发了JavaScript

1996年

微软发布了和JavaScript兼容的 JScript

1997年

ECMAScript 第1版(ECMA-262)

1998年

ECMAScript 第2版

1998年

DOM Level1 的制定

1998年

新型语言 DHTML 登场

1999年

ECMAScript 第3版

2000年

DOM Level2 的制定

2002年

ISO/ IEC 16262:2002 的确立

2004年

DOM Level3 的制定

2005年

新型语言 AJAX 登场

2009年

ECMAScript 第5版

2009年

新型语言 HTML5 登场

最初,JavaScript 所获得的评价并不都是正面的。当时的 PC 性能很弱,JavaScript 的实现也不够成熟,很多人觉得运行了 JavaScript 的页面会变得十分缓慢,浏览器也会变得不稳定。甚至曾经有不少人大力呼吁,应该在浏览器中取消 JavaScript。

随着 Web 使用的普及,要求改善浏览器用户界面的呼声越来越高。因此尽管速度不快,JavaScript 的重要性还是在逐步提升。在这段时期,网景公司以及微软都在不断地进行技术革新,微软逐渐取得技术上的领先地位。由微软等公司提出的 DHTML(动态 HTML)是 JavaScript 的基础。DHTML 是一种为了推广而命名的方便说法,意指 DOM 和 CSS 等 W3C 标准与 JavaScript 相结合后,所能提供的丰富的浏览器用户界面。

就这样,在 2000 年前后,JavaScript 相关的各种技术基本准备就绪。2005 年前后,Web 应用得到广泛普及。特别是出现了以谷歌为首提出的异步 JavaScript(之后统称为 AJAX,即 Asynchronous JavaScript and XML),使接近桌面应用的复杂用户界面得以实现。

在 Web 应用变得越来越复杂的过程中,JavaScript 的代码规模与复杂性也日益提升,prototype.js、jQuery 等各种 JavaScript 库相应登场。可以说,2005 年之后的几年是 JavaScript 的繁荣期。

在这一繁荣期中,还有另一个不能忽视的成员,即 Mozilla 基金会(Mozilla Foundation)。Mozilla 基金会的历史可以追随到网景公司时期。Mozilla 的开源浏览器 Firefox 的坚实发展所带来的 JavaScript 的速度改善,确实是 JavaScript 繁荣的一大主要原因。说到 JavaScript 的性能提升,谷歌在 2008 年与浏览器 Google Chrome 一同发布的 JavaScript 引擎 v8 也是一个重要的契机。在此之后,发生了各种 JavaScript 实现方式之间比拼速度的状况。

ECMAScript

JavaScript 的标准化

上面提到,JavaScript 是由网景公司提出的。之后,微软开发了和 JavaScript 相兼容的 JScript 并将其应用于 Internet Explorer 中。不过,人们通常将两者统称为 JavaScript。

为了防止因两家公司独自开发而导致 JavaScript 分裂以及其他一些问题,网景公司提出了名为 Ecma International 的 JavaScript 标准化组织。这一标准语言的名称就是 ECMAScript。由于将语言规则的制定权交给了中立的标准化组织,网景公司放弃了对 JavaScript 的垄断地位,JavaScript 因此具备了标准化程序设计语言所必须的安定感。对于开发者来说,标准的程序设计语言不会随特定企业的想法而轻易改变,也更令人安心。这是因为如果一种语言由某一企业所控制,可能会发生开发终止或是需要收费使用的情况。

ECMAScript 的标准编号是 ECMA-262,并在之后获得了 ISO 的承认(ISO-16262)。通俗来讲,就是得到了 ISO 的权威认证。根据 ECMAScript 标准,网景公司的 JavaScript 被重新定义为一种符合 ECMAScript 标准的程序设计语言。微软的 JScript 亦然。即使之后 JavaScript 的开发主体由网景公司变为了 Mozilla 基金会,这一定义也没有改变。

之后还出现了其他 ECMAScript 的具体实现,不过现在都将它们统称为 JavaScript 实现。严格来说,由网景公司开发、现由 Mozilla 基金会继续发展的语言称为 JavaScript,其他 ECMAScript 标准的实现方式称为 JavaScript 的兼容实现方式。不过这样区分的意义并不大,所以我们将这些统称为 JavaScript 实现方式。目前,具代表性的 JavaScript 实现方式一方面以标准为主,一方面也在独立发展。也就是说,它们在提供了 ECMAScript 功能的基础上,继续提供其他便捷功能。事实上,JavaScript 的具体实现大部分都是 ECMAScript 的超集。因此,如果要保证可移植性,只要做到在代码中仅使用 ECMAScript 标准所包含的功能即可。

被放弃的 ECMAScript 第4版

表1(JavaScript 简史)中并没有 ECMAScript 第4版,这是因为 ECMAScript 第 4 版没能符合要求而最终被放弃了。

ECMAScript 第 3 版是在 1999 年提出的。一方面可以说 JavaScript 在 10 年间保持了稳定不变,但另一方面也意味着它的标准止于 10 年之前,已经停止了前进。一般来说,1999 以后的 ECMAScript 第 3 版以及 JavaScript 1.5 版被作为默认标准,即使 JavaScript 增加了新功能也被视为增强功能。官方的意见是为了与标准相兼容,不应该使用新功能。标准化有积极的一面,但同时又由于其发展过于缓慢,导致了 JavaScript 的具体实现往往增加了很多独有功能,造成了代码可移植性降低的不良后果。

在大约 10 年的时间里(1999 年至 2008 年),ECMAScript 第 4 版的制定工作一直在进行,原本计划向业已规范有序的标准中进一步加入大量增强功能。在第 4 版中甚至有引入“类”的概念这样大胆的标准变更计划。然而,2008 年的标准化工作大会放弃了大幅度变更标准的计划,转为在第 3 版的基础上进行渐进式改进。于是,在 2009 年直接发布了和第 3 版标准差异不大的第 5 版。

由于 ECMAScript 第 5 版的保守,JavaScript 1.6 版中很多新增功能的处境也变得微妙起来。虽然其中也有一些功能仍然被 ECMAScript 第 5 版采用,但其大部分都没能被接受。因此,虽说只要遵循 ECMAScript 标准依然可以随意使用,但 JavaScript 1.6 版实际上成为了一种独立的 JavaScript 增强版本。总之,如果要遵循标准或是保证可移植性的话,就不应该使用那些功能。

JavaScript的版本

正如上面所讲,JavaScript 是一种符合 ECMAScript 标准的程序设计语言。而事实上,往往是先由 JavaScript 实现某一功能,ECMAScript 才对其进行标准化处理。由于历史原因,Mozilla 基金会所开发的 JavaScript(严格意义上的真正的 JavaScript)常常会在标准化之前就加入一些新功能。

JavaScript 版本和 ECMAScript 版本的对应关系如表 2 所示。

表2 JavaScript 的版本

JavaScript 1.0

Navigator 2.0

JavaScript 1.1

Navigator 3.0,以此为基础开始了 ECMAScript 的标准化

JavaScript 1.2

Navigator 4.0-4.05,ECMAScript版本大致相当于第 1 版标准

JavaScript 1.3

Navigator 4.06-4.7x,ECMAScript版本为第 1 版标准

JavaScript 1.4

第 1 版标准

JavaScript 1.5

Navigator 6.0,Mozilla,ECMAScript版本为第 3 版标准

JavaScript 1.6

Firefox 1.5,相当于 ECMAScript 第 4 版的先行版

JavaScript 1.7

Firefox 2,相当于 ECMAScript 第 4 版的先行版

JavaScript 1.8

Firefox 3,相当于 ECMAScript 第 4 版的先行

JavaScript 1.8.1

Firefox 3.5,ECMAScript版本大致相当于第 5 版标准

JavaScript 1.8.5

Firefox 4.0,ECMAScript版本为第 5 版标准

JavaScript 实现方式

表3 列出了搭载了 JavaScript 引擎的具有代表性的浏览器。虽说这几年给每个版本附上一个开发代号的做法很流行,不过在这里还是使用各自的通称。

表3 浏览器和 JavaScript 实现方式

FireFox:SpiderMonkey

Internet Explorer:JScript

Safari:JavaScriptCore

Chrome:v8

Carakan:Carakan(最新版的开发代号)

客户端 JavaScript 代码的可移植性

JavaScript 编程中有一个很麻烦的问题,即在不同的浏览器中其执行方式会有所不同。前面曾提到 JavaScript 早期的评价并不太好,其中一个很重要的原因就是,JavaScript 在不同的浏览器中的执行方式的确会有差别。许多开发者怨声不断,逐渐造成了一种 JavaScript 编程非常麻烦的印象。但如果冷静下来思考一下,就会发现 JavaScript 其实并没有所说的那么夸张。

稍加了解就会发现,C/C++ 等其他一些语言,和如今的 JavaScript 一样,都衍生出了多种不同的实现方式。它们虽然在遵循语言标准时,能够实现一定程度的可移植性,但对于不同平台(OS)的情况,其可移植性完全无法令人满意。PHP、Perl、Python、Ruby 等流行的脚本语言虽然在不同平台间也有着很高的可移植性,但这是因为它们基本上只有唯一一种实现方式。Java 确实有多种实现方式,也实现了很强的可移植性,不过这是由于它最初就在保证可移植性上花费了很大的精力,所以算是一个例外。把 JavaScript 和 Java 作对比来得出其可移植性不强未免有些不妥。

影响客户端 JavaScript 可移植性的原因主要有两点。

  • JavaScript 语言实现方式的不同

  • 渲染引擎的差别(DOM 或是 CSS 的解释不同)

在实际中,后者更为麻烦,并由此产生了许多不良开发方式。要解决 JavaScript 语言实现方式差异的关键在于 ECMAScript,因为 ECMAScript 作为一种标准,有明确的规定。现在大多数有名的 JavaScript 实现都基于 ECMAScript 标准,所以只要书写符合 ECMAScript 标准的代码,就能够在很大程度上提高可移植性。

另一方面,渲染引擎没有像程序设计语言一样被标准化,所以相当难办。不过有一个被称为 Acid 的测试,可以用于减少这一不同引擎之间执行方式不同的问题。

http://www.webstardards.org/action/acid3/

Acid 并不像 ECMAScript 那样有明确标准,它会对浏览器进行特定测试,根据返回的结果是否相同来判断代码的执行情况。该测试可以用于判断 JavaScript、DOM、CSS 等各种客户端 JavaScript 的执行情况。现在很多的浏览器都以符合 Acid 标准(即可以通过 Acid 测试)为目标。在撰写本文时,Acid 的版本号为 3。用浏览器登录下面的 URL 地址就能够获得测试得分:

http://acid3.acidtests.org/

刚刚已经介绍了有关客户端 JavaScript 可移植性改进的内容。很可惜,情况尚不乐观。首先是浏览器版本陈旧的问题。之前提到的 ECMAScript 标准以及 Acid 测试标准都是基于最新版本的浏览器的。如果需要支持旧版本的浏览器,则仍然要注意执行方式上的差异。

另一个问题是对 PC 之外的设备的支持。如今的智能手机、平板电脑以及智能电视,原本就有着不同的用户界面。虽说客户端 JavaScript 的可移植性确实在逐渐提高,但如果考虑到现在越来越普及的非 PC 设备的情况,可以说现在正处于一种过去未曾有过的混乱状态。所幸非 PC 设备的渲染引擎基本上被 WebKit 所垄断,总算使问题稍有缓解。

JavaScript 运行环境

核心语言

由于人们对 JavaScript 的印象大多都是客户端 JavaScript,所以常认为 JavaScript 编程和 DOM 编程是不可分割的。

简单说来,DOM 编程就是浏览器和用户之间的接口,可以在浏览器上显示内容或是反馈用户的点击操作。尽管在浏览器上两者的联系紧密,但 JavaScript 和 DOM 并不是不可分割的,它们的语言标准相互独立。DOM 对客户端 JavaScript 来说,仅仅是一宿主对象。大家对宿主对象一词可能并不熟悉,只要把它理解为类似于其他程序设计语言的外部库的概念即可,也就是语言中可以更换的部分。而核心语言则是特指 JavaScript 中不可被替代的功能。

JavaScript 的核心语言和宿主对象的概念如下图所示(图1)。

{%}

图1 Web 应用程序的组成结构

宿主对象

如图 1 所示,JavaScript 中对于不同的运行环境,有着不同的内置宿主对象。这是由于 JavaScript 是被作为一种扩展语言而设计的。对于通用程序设计语言,开发者必须自己开发运行时的上下文环境。正因如此,那些语言才有了通用程序设计语言的名称。另一方面,扩展语言是在内建对象的应用程序(宿主环境)中运行程序的。宿主应用程序会在这时收到一些运行时的上下文信息。JavaScript 会以全局对象作为根节点的对象树的形式,接受这些上下文信息。在启动时,JavaScript 从宿主环境获取的对象树就被称为宿主对象。

从 JavaScript 代码的角度看来,全局对象在程序启动前就已经存在了。客户端 JavaScript 的全局对象被称作 window 对象。

**JavaScript 的特点

** JavaScript 程序设计语言有如下几个特点:

  • 解释型语言

    JavaScript 是一种解释型语言,和解释型语言相对的是编译型语言。解释型语言直接在运行环境中执行代码,所以一般来说,与编译型语言相比,解释型语言的开发更为容易。特别是 JavaScript,其运行环境是已经普及的浏览器,所以能够很容易地尝试开发。这是其他程序设计语言所不能比拟的。

    解释型语言的劣势在于,其运行速度通常都会慢于编译型语言,不过这也只是理论上的情况。现在,解释型语言和编译型语言之间的界线正在变得越来越模糊。编译型语言在有了足够快速的编译器和功能强大的开发环境之后,也能实现和解释型语言相匹敌的开发难易度。同时,解释型语言由于使用了 JIT(Just In Time)这种能够在运行中进行编译的技术,使得运行速度得以改善。

    如今,在选择程序设计语言时,比起选择编译型语言还是解释型语言,更重要的是考虑语言的设计目的。是为了使开发过程变得轻松还是为了提高执行效率,语言最初的设计理念不同,其性质自然会有差异。设计 JavaScript 之初,优先考虑的是使开发过程变得轻松,因此提供了多种特性。

  • 类似于 C 和 Java 的语法结构

    JavaScript 的语法结构与 C 和 Java 相似。JavaScript 同样有 if 或 while 这类关键字,其语法结也与 C 和 Java 类似。它们乍一看很像,因此有这些语言开发经验的人很容易就能熟悉 JavaScript。不过需要注意的是,它们之间的相似性其实并不如表面看起来的那么强。

  • 动态语言

    JavaScript 与 C 和 Java 所不同的一点在于,JavaScript 是一种动态语言,将在之后详述。单从代码的角度看,动态语言的变量和函数是不指定返回值类型的。JavaScript 之所以被设计成动态语言,和选择将其设计为解释型语言的理由一样,都是优先考虑了开发难易度的结果。对解释型语言以及动态语言的特性的喜好虽然见仁见智,但语言本身并没有高下优劣之分。

  • 基于原型的面向对象

    解释型动态语言并不少见,现有的较为知名的脚本语言大多都属于这一类型。不过基于原型的面向对象特性,使得 JavaScript 与它们有所不同。基于原型的面向对象特性和基于类的面向对象特性是有所差别的,在此请先了解这一点即可,更为详细的内容将会在之后详述。目前,被称为面向对象语言的程序设计语言,大多提供了基于类的面向对象语言功能。JavaScript 虽然并不是第一个采用基于原型的面向对象特性的语言,不过可以说是这类语言中最为著名的。同样,基于原型与基于类的面向对象语言之间的差异,也主要是个人喜好的区别,而并非是孰优孰劣的问题。

  • 字面量的表现能力

    字面量的表现能力是 JavaScript 开发生产力得以提高的一个重要原因。在 Perl 之后,很多语言都提供了功能强大的字面量功能。虽然其中表现突出的不止 JavaScript 一种,不过由于它的字面量功能相对来说非常优秀,所以作为语言特点之一列举于此。

  • 函数式编程

    最后来介绍一下函数式编程。函数式编程是一种历史悠久,而又在最近颇为热门的话题。函数式编程在面向对象一词诞生以前就已经存在,不过它在很长的一段时间里都被隐藏于过程式编程(面向对象也是过程式编程的一种)的概念之下。然而现在这种状况正在逐步发生改变,JavaScript 正是这一改变过程中的一部分。尽管 JavaScript 能直接支持的程序设计范式在本质上还是过程式的,但由于具备了匿名函数,可以把函数作为对象来使用,所以同时也能够支持函数式编程。

函数基础

函数的定义

JavaScript 中的函数是一种类似于 Java 中方法的语言功能,不过它可以独立于类进行定义,所以从表面上来看,反而和 C 语言或是 PHP 的函数、Perl 的子程序更为相似。不过,JavaScript 中的函数和它们在本质上是不同的。

本处仅对 JavaScript 的函数进行概要性的说明。在基本使用方式上,JavaScript 中的函数和其他程序设计语言中被称为函数或子程序的概念并没有什么不同。函数是由一连串的子程序(语句的集合)所组成的,可以被外部程序调用。向函数传递参数之后,函数可以返回一定的值。

通常情况下,JavaScript 代码是自上而下执行的,不过函数体内部的代码则不是这样。如果只是对函数进行了声明,其中的代码并不会执行。只有在调用函数时才会执行函数体内部的代码(代码清单1)。

代码清单1 包含函数的代码的执行顺序

print('1');
function f() {    // 声明函数
    print('2');
}
print('3');
f();              // 调用函数


// 代码清单2的运行结果
1
3
2

函数的声明与调用

可以通过函数声明语句来定义一个函数。函数声明语句以关键字 function 开始,其后跟有函数名、参数列表和函数体。其语法如下所示:

// 函数声明语句的语法
function 函数名 ( 参数, 参数, ……) {
 函数体
}

代码清单2是个具体例子,其中函数名为 sum,参数名为 ab。函数声明中所写的参数称为形参(形式参数)。代码清单2中的函数 sum 对两个参数做了加法运算,并通过 return 语句返回结果。

代码清单2 函数 sum 的声明

function sum (a, b) {
    return Number(a) + Number(b);
}

可以像下面这样来调用函数 sum。调用函数时,传递给函数的参数称为实参(实际参数)。下面代码中以 3 和 4 作为实参调用了函数 sum。

// 函数sum 的调用
js> sum (3, 4);
7

函数声明时不必指定形参的类型。任何类型的值都可以作为实参传递,因而开发者在设计函数时需要考虑接收错误类型的值的情况。此外,形参的数量和实参的数量可以不一致,这一点将在之后再具体说明。JavaScript 的这些特性,与始终严格检查参数类型的 Java 形成了鲜明的对比。因此,在 JavaScript 中自然也就不存在函数重载这一特性(即可以存在多个参数不同的同名函数)。

不过 JavaScript 的变量本身就没有类型可言,所以形参没有类型也不奇怪。

匿名函数

还可以通过匿名函数表达式来定义一个函数。其语法形式为在 function 后跟可以省略的函数名、参数列表以及函数体。其语法如下所示:

// 匿名函数的语法
function ( 参数, 参数, ……) {
  函数体
}
function 函数名 ( 参数, 参数, ……) {
  函数体
}

可以看到,函数声明语句和匿名函数表达式在语法上几乎一模一样,唯一的区别仅仅是能否省略函数名称而已。不过,因为匿名函数表达式是一种表达式而非语句,所以也可以在表达式内使用。另外由于它是表达式因此也会有返回值。匿名函数的返回值是一个 Function 对象的引用。把它简单理解为返回一个函数也没有问题。

不过请不要因为它们都是表达式,而将匿名函数表达式与函数调用表达式相混淆。函数调用表达式在大部分程序设计语言中都是存在的,而匿名函数表达式在一些程序设计语言中并不存在(至少 Java 中的方法是无法实现这样的功能的)。其实,通过表达式来定义一个函数并不是什么新的功能。早在与 JavaScript 有些类似的 Lisp 语言的时代,这种功能就已经存在,并且在一些比较新的程序设计语言中,这一功能正在变得越来越常见。

匿名函数表达式的使用方式如代码清单3所示。赋值表达式右侧的就是匿名函数表达式。

代码清单3 匿名函数表达式的例子

var sum2 = function (a, b) {
    return Number(a) + Number(b)
}

sum2 的前面是 var,所以它是一个变量名。以 function 开始的匿名函数表达式将返回一个函数。也就是说,代码清单3的含义是,将 Function 对象的一个引用赋值给变量 sum2。可以像下面这样来调用 变量 sum2 所引用的函数。

// 调用函数sum2
js> sum2(3, 4);
7

这段代码和代码清单2中调用函数 sum 的方式没有区别。也就是说,代码清单2 中的函数声明语句,和代码清单3中赋值表达式的作用是相同的,都会将匿名函数表达式赋值给变量。两者都是在生成一个没有名称的函数体(Function 对象)之后,再赋予其一个名称。目前只要认为代码清单2和3 中的语句是相同的就可以了。

还可以像下面这样,在右侧书写通过函数声明语句进行定义的函数,以将其赋值给左值,这样就可以通过被赋值对象的名称来调用该函数。将其理解为 sum 这一名称持有该函数对象的引用即可。至此,读者或许会觉得变量名和函数名之间的分界很模糊,而事实也确实如此,我们之后将会对此进行详述。

// 在表达式右侧书写代码清单2中的函数名
js> var sum3 = sum;
// 调用函数sum3
js> sum3(3, 4);
7

函数是一种对象

JavaScript 中的函数和 Java 中的方法或 C 语言中的函数的最大不同在于,JavaScript 中的函数也是一种对象。下面将阐述对象的概念,届时可以了解到对象在本质上是没有名称的。而对于函数来说也是如此,因为函数本身也是一种对象。

正如变量存在的意义是为了调用没有名称的对象,函数名存在的意义是为了调用没有名称的函数。因此,变量名和函数名实质上是相同的。这一点在之后会再次说明。虽然有时也需要区别对待变量名和函数名,不过这里为方便起见,暂且认为两者在本质上是相同的。

JavaScript 的函数是一种对象,不过并不是说所有的对象都是函数。函数是一种包含了可执行代码,并能够被其他代码调用的特殊的对象。

代码书写风格

无论是哪种程序设计语言,都有其代码书写风格。虽然不遵循代码书写风格也能写出可以运行的代码,但是这会为之后阅读代码时增加不必要的麻烦。因此遵循代码书写风格是非常重要的。

JavaScript 由于其特殊的历史与定位,有着略显独特的代码书写风格。造成这一局面的背景之一,和客户端 JavaScript 的历史有关。对于客户端代码来说,代码体积的大小不但会影响运行速度,还会直接影响网络传输的性能。在 Web 的发展历史中,尤其是早期,如何减少网络数据传输量是一个重要的课题(至今仍然非常重要)。因此,JavaScript 中有着大量以减少代码书写量为目的的代码书写风格。乍一看,简洁书写似乎是一件好事,但事实上,也存在着一些看似取巧实则糟糕的做法。说到底,代码书写风格是历史的产物,不能仅仅根据是否正确来判断代码书写风格的好坏,而应该去尝试接受这些既定事实。

另一背景则和 JavaScript 普及的历史有关。其他很多程序设计语言在被广泛普及之前,往往都会有少数优秀的开发者首先使用。通常在这一时期,该语言的代码书写风格会初步成形,然后在随后更为广泛的普及过程中发生一些变化。而 JavaScript 的情况并非如此。在其普及初期,对 JavaScript 感兴趣的主要是那些书写 HTML 的网页设计师,或者一些主用其他语言的开发者,他们仅仅把 JavaScript 作为一种临时的替代品来使用。因此,JavaScript 没有属于自己的核心代码书写风格,却有着很多模仿其他语言而来的代码书写风格。比如,类似于 Java 代码风格的 JavaScript 代码,又或是类似于 PHP 代码风格的 JavaScript 代码,诸如此类。不过最近几年,具有 JavaScript 自身特点的代码书写风格正在逐渐普及。

 

{%}

《JavaScript编程全解》全方位地介绍了JavaScript开发中的各个主题,无论是前端还是后端的JavaScript开发者都可以在本书中找到自己需要的内容。本书对HTML5、Web API、Node.js及WebSocket等最新的热门技术也作了深入浅出的介绍,并提供了大量实际应用范例。本文节选自《JavaScript编程全解》

为什么我们需要的新语言是Go

{%}

作者/ 许式伟

许式伟是七牛云存储创始人兼CEO,曾任盛大创新院资深研究员、金山软件技术总监、WPS Office 2005首席架构师。开源爱好者,发布过包括WINX、TPL等十余个C++开源项目,拥有超过15年的C/C++开发经验。在接触Go语言后即可被其大道至简、少即是多的设计哲学所倾倒。七牛云存储是国内第一个吃螃蟹的团队,核心服务完全采用Go语言实现。

编程语言已经非常多,偏性能敏感的编译型语言有 C、C++、Java、C#、Delphi和Objective-C等,偏快速业务开发的动态解析型语言有PHP、Python、Perl、Ruby、JavaScript和Lua等,面向特定领域的语言有Erlang、R和MATLAB等,那么我们为什么需要 Go这样一门新语言呢?

在2000年前的单机时代,C语言是编程之王。随着机器性能的提升、软件规模与复杂度的提高,Java逐步取代了C的位置。尽管看起来Java已经深获人心,但Java编程的体验并未尽如人意。历年来的编程语言排行榜(如图1所示)显示,Java语言的市场份额在逐步下跌,并趋近于C语言的水平,显示了这门语言后劲不足。

{%}

图1 编程语言排行榜1

1数据来源:http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

Go语言官方自称,之所以开发Go 语言,是因为“近10年来开发程序之难让我们有点沮丧”。这一定位暗示了Go语言希望取代C和Java的地位,成为最流行的通用开发语言。

Go希望成为互联网时代的C语言。多数系统级语言(包括Java和C#)的根本编程哲学来源于C++,将C++的面向对象进一步发扬光大。但是Go语言的设计者却有不同的看法,他们认为C++ 真的没啥好学的,值得学习的是C语言。C语言经久不衰的根源是它足够简单。因此,Go语言也要足够简单!

那么,互联网时代的C语言需要考虑哪些关键问题呢?

首先,并行与分布式支持。多核化和集群化是互联网时代的典型特征。作为一个互联网时代的C语言,必须要让这门语言操作多核计算机与计算机集群如同操作单机一样容易。

其次,软件工程支持。工程规模不断扩大是产业发展的必然趋势。单机时代语言可以只关心问题本身的解决,而互联网时代的C语言还需要考虑软件品质保障和团队协作相关的话题。

最后,编程哲学的重塑。计算机软件经历了数十年的发展,形成了面向对象等多种学术流派。什么才是最佳的编程实践?作为互联网时代的C语言,需要回答这个问题。

接下来我们来聊聊Go语言在这些话题上是如何应对的。

并发与分布式

多核化和集群化是互联网时代的典型特征,那语言需要哪些特性来应对这些特征呢?

第一个话题是并发执行的“执行体”。执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。多数语言在语法层面并不直接支持协程,而通过库的方式支持的协程的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的协程中调用一个同步IO操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行协程,从而无法真正达到协程本身期望达到的目标。

Go语言在语言级别支持协程,叫goroutine。Go语言标准库提供的所有系统调用(syscall)操作,当然也包括所有同步IO操作,都会出让CPU给其他goroutine,这让事情变得非常简单。我们对比一下Java和Go,近距离观摩下两者对“执行体”的支持。

为了简化,我们在样例中使用的是Java标准库中的线程,而不是协程,具体代码如下:

public class MyThread implements Runnable {

    String arg;

    public MyThread(String a) {
        arg = a;
    }

 public void run() {
        // ...
    }

    public static void main(String[] args) {
        new Thread(new MyThread("test")).start();
        // ...
    }
}

相同功能的代码,在Go语言中是这样的:

func run(arg string) {
    // ...
}

func main() {
    go run("test")
    ...
}

对比非常鲜明。我相信你已经明白为什么Go语言会叫Go语言了:Go语言献给这个时代最好的礼物,就是加了go这个关键字。当然也有人会说,叫Go语言是因为它是Google出的。好吧,这也是个不错的闲聊主题。

第二个话题是“执行体间的通信”。执行体间的通信包含几个方式:

  • 执行体之间的互斥与同步

  • 执行体之间的消息传递

先说“执行体之间的互斥与同步”。当执行体之间存在共享资源(一般是共享内存)时,为保证内存访问逻辑的确定性,需要对访问该共享资源的相关执行体进行互斥。当多个执行体之间的逻辑存在时序上的依赖时,也往往需要在执行体之间进行同步。互斥与同步是执行体间最基础的交互方式。

多数语言在库层面提供了线程间的互斥与同步支持,那么协程之间的互斥与同步呢?呃,不好意思,没有。事实上多数语言标准库中连协程都是看不到的。

再说“执行体之间的消息传递”。在并发编程模型的选择上,有两个流派,一个是共享内存模型,一个是消息传递模型。多数传统语言选择了前者,少数语言选择后者,其中选择“消息传递模型”的最典型代表是Erlang语言。业界有专门的术语叫“Erlang风格的并发模型”,其主体思想是两点:一是“轻量级的进程(Erlang中‘进程’这个术语就是我们上面说的‘执行体’)”,二是“消息乃进程间通信的唯一方式”。当执行体之间需要相互传递消息时,通常需要基于一个消息队列(message queue)或者进程邮箱(process mail box)这样的设施进行通信。

Go语言推荐采用“Erlang风格的并发模型”的编程范式,尽管传统的“共享内存模型”仍然被保留,允许适度地使用。在Go语言中内置了消息队列的支持,只不过它叫通道(channel)。两个goroutine之间可以通过通道来进行交互。

软件工程

单机时代的语言可以只关心问题本身的解决,但是随着工程规模的不断扩大,软件复杂度的不断增加,软件工程也成为语言设计层面要考虑的重要课题。多数软件需要一个团队共同去完成,在团队协作的过程中,人们需要建立统一的交互语言来降低沟通的成本。规范化体现在多个层面,如:

  • 代码风格规范

  • 错误处理规范

  • 包管理

  • 契约规范(接口)

  • 单元测试规范

  • 功能开发的流程规范

Go语言很可能是第一个将代码风格强制统一的语言,例如Go语言要求public的变量必须以大写字母开头,private变量则以小写字母开头,这种做法不仅免除了publicprivate关键字,更重要的是统一了命名风格。

另外,Go语言对{ }应该怎么写进行了强制,比如以下风格是正确的:

if expression {
    ...
}

但下面这个写法就是错误的:

if expression
{
    ...
}

而C和Java语言中则对花括号的位置没有任何要求。哪种更有利,这个见仁见智。但很显然的是,所有的Go代码的花括号位置肯定是非常统一的。

最有意思的其实还是 Go 语言首创的错误处理规范:

f, err := os.Open(filename)
if err != nil {
    log.Println("Open file failed:", err)
    return
}
defer f.Close()
... // 操作已经打开的f文件

这里有两个关键点。其一是defer关键字。defer语句的含义是不管程序是否出现异常,均在函数退出时自动执行相关代码。在上面的例子中,正是因为有了defer,才使得无论后续是否会出现异常,都可以确保文件被正确关闭。其二是Go语言的函数允许返回多个值。大多数函数的最后一个返回值会为error类型,以在错误情况下返回详细信息。error类型只是一个系统内置的interface,如下:

type error interface {
    Error() string
}

有了error类型,程序出现错误的逻辑看起来就相当统一。

在Java中,你可能这样写代码来保证资源正确释放:

Connection conn = ...;
try {
    Statement stmt = ...;
    try {
        ResultSet rset = ...;
        try {
            ... // 正常代码
        }
        finally {
            rset.close();
        }
    }
    finally {
        stmt.close();
    }
}
finally {
    conn.close();
}

完成同样的功能,相应的Go代码只需要写成这样:

conn := ...
defer conn.Close()

stmt := ...
defer stmt.Close()

rset := ...
defer rset.Close()
... // 正常代码

对比两段代码,Go语言处理错误的优势显而易见。当然,其实Go语言带给我们的惊喜还有很多,后续有机会我们可以就某个更具体的话题详细展开来谈一谈。

编程哲学

计算机软件经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程、函数式编程、面向消息编程等,这些思想究竟孰优孰劣,众说纷纭。

C语言是纯过程式的,这和它产生的历史背景有关。Java语言则是激进的面向对象主义推崇者,典型表现是它不能容忍体系里存在孤立的函数。而Go语言没有去否认任何一方,而是用批判吸收的眼光,将所有编程思想做了一次梳理,融合众家之长,但时刻警惕特性复杂化,极力维持语言特性的简洁,力求小而精。

从编程范式的角度来说,Go语言是变革派,而不是改良派。

对于C++、Java和C#等语言为代表的面向对象(OO)思想体系,Go语言总体来说持保守态度,有限吸收。

首先,Go语言反对函数和操作符重载(overload),而C++、Java和C#都允许出现同名函数或操作符,只要它们的参数列表不同。虽然重载解决了一小部分面向对象编程(OOP)的问题,但同样给这些语言带来了极大的负担。而Go语言有着完全不同的设计哲学,既然函数重载带来了负担,并且这个特性并不对解决任何问题有显著的价值,那么Go就不提供它。

其次,Go语言支持类、类成员方法、类的组合,但反对继承,反对虚函数(virtual function)和虚函数重载。确切地说,Go也提供了继承,只不过是采用了组合的文法来提供:

type Foo struct {
    Base
    ...
}

func (foo *Foo) Bar() {
    ...
}

再次,Go语言也放弃了构造函数(constructor)和析构函数(destructor)。由于Go语言中没有虚函数,也就没有vptr,支持构造函数和析构函数就没有太大的价值。本着“如果一个特性并不对解决任何问题有显著的价值,那么Go就不提供它”的原则,构造函数和析构函数就这样被Go语言的作者们干掉了。

在放弃了大量的OOP特性后,Go语言送上了一份非常棒的礼物:接口(interface)。你可能会说,除了C这么原始的语言外,还有什么语言没有接口呢?是的,多数语言都提供接口,但它们的接口都不同于Go语言的接口。

Go语言中的接口与其他语言最大的一点区别是它的非侵入性。在C++、Java和C#中,为了实现一个接口,你需要从该接口继承,具体代码如下:

class Foo implements IFoo { // Java文法
    ...
}

class Foo : public IFoo { // C++文法
    ...
}

IFoo* foo = new Foo;

在Go语言中,实现类的时候无需从接口派生,具体代码如下:

type Foo struct { // Go 文法
    ...
}

var foo IFoo = new(Foo)

只要Foo实现了接口IFoo要求的所有方法,就实现了该接口,可以进行赋值。

Go语言的非侵入式接口,看似只是做了很小的文法调整,实则影响深远。

其一,Go语言的标准库再也不需要绘制类库的继承树图。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。

其二,不用再纠结接口需要拆得多细才合理,比如我们实现了File类,它有下面这些方法:

Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error

那么,到底是应该定义一个IFile接口,还是应该定义一系列的IReaderIWriterISeekerICloser接口,然后让File从它们派生好呢?事实上,脱离了实际的用户场景,讨论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它呢?

其三,不用为了实现一个接口而专门导入一个包,而目的仅仅是引用其中的某个接口的定义。在Go语言中,只要两个接口拥有相同的方法列表,那么它们就是等同的,可以相互赋值,如对于以下两个接口,第一个接口:

package one

type ReadWriter interface {
    Read(buf [] byte) (n int, err error)
    Write(buf [] byte) (n int, err error)
}

第二个接口:

package two

type IStream interface {
    Write(buf [] byte) (n int, err error)
    Read(buf [] byte) (n int, err error)
}

这里我们定义了两个接口,一个叫one.ReadWriter,一个叫two.IStream,两者都定义了Read()Write()方法,只是定义的次序相反。one.ReadWriter先定义了Read()再定义Write(),而two.IStream反之。

在Go语言中,这两个接口实际上并无区别,因为:

  • 任何实现了one.ReadWriter接口的类,均实现了two.IStream

  • 任何one.ReadWriter接口对象可赋值给two.IStream,反之亦然;

  • 在任何地方使用one.ReadWriter接口,与使用two.IStream并无差异。

所以在Go语言中,为了引用另一个包中的接口而导入这个包的做法是不被推荐的。因为多引用一个外部的包,就意味着更多的耦合。

除了OOP外,近年出现了一些小众的编程哲学,Go语言对这些思想亦有所吸收。例如,Go语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了以Erlang语言为代表的面向消息编程思想,支持goroutine和通道,并推荐使用消息而不是共享内存来进行并发编程。总体来说,Go语言是一个非常现代化的语言,精小但非常强大。

小结

在十余年的技术生涯中,我接触过、使用过、喜爱过不同的编程语言,但总体而言,Go语言的出现是最让我兴奋的事情。我个人对未来10年编程语言排行榜的趋势判断如下:

  • Java语言的份额继续下滑,并最终被C和Go语言超越;

  • C语言将长居编程榜第二的位置,并有望在Go取代Java前重获语言榜第一的宝座;

  • Go语言最终会取代Java,居于编程榜之首。

由七牛云存储团队编著的《Go语言编程》将尽可能展现出Go语言的迷人魅力。希望本书能够让更多人理解这门语言,热爱这门语言,让这门优秀的语言能够落到实处,把程序员从以往繁杂的语言细节中解放出来,集中精力开发更加优秀的系统软件。

 

{%}

《Go语言编程》首先概览了Go语言的诞生和发展历程,从面向过程编程特性入手介绍Go语言的基础用法,让有一定C语言基础的读者可以非常迅速地入门并开始上手用Go语言来解决实际问题,之后介绍了Go语言简洁却又无比强大的面向对象编程特性和并发编程能力,至此读者已经可以理解为什么Go语言是为互联网时代而生的语言。从实用性角度出发,本书还介绍了Go语言标准库和配套工具的用法,包括安全编程、网络编程、工程管理工具等。对于希望对Go语言有更深入了解的读者,我们也特别组织了一系列进阶话题,包括语言交互性、链接符号、goroutine机理和接口机制等。本文节选自《Go语言编程》