宠文网

奔跑吧,程序员

宠文网 > 科普学习 > 奔跑吧,程序员

5.4选择编程语言

书籍名:《奔跑吧,程序员》    作者:叶夫根尼.布里克曼
    《奔跑吧,程序员》章节:5.4选择编程语言,宠文网网友提供全文无弹窗免费在线阅读。!



选择编程语言通常是要做的第一个技术上的决定,同时也是对其他决定最有影响的一个。“熟悉什么就用什么”的原则意味着大多数创业公司开始所使用的都是创始人最了解的语言。然而,随着创业公司的发展和进化,通常都要引入其他语言。例如,Twitter开始时用的是Ruby,但是在最近几年,他们已经把许多服务迁移到Scala。但为什么是Scala?为什么不是Python、Java、Haskell或者其他语言?要回答这个问题,需要弄清楚每种编程语言之间的关键差别,包括编程范式、适用问题、性能、适用文化和生产效率。



5.4.1  编程范式


每一种编程语言对于如何解决问题都有不同的哲学。我们可以把编程语言的范式当作是该语言的词汇和语法,决定了你如何说和可以怎么说。目前很少有确切的证据表明某种范式优于其他范式,但某些范式相比其他范式可以更方便地表达一些想法。接下来的几节将讨论少数几种最主流的范式之间的利弊,包括面向对象编程、函数式编程、静态类型和自动内存管理。

1.  面向对象编程

面向对象编程(object-oriented  programming,OOP)尝试将世界万物用对象(即封装了数据和行为的数据结构)来建模。OOP在近20年来一直都占据着编程范式中的统治地位,世界上最流行的一些编程语言都使用了这一范式,包括C++、C#、Java、JavaScript、Ruby和Python。

面向对象编程之所以这么流行,部分因为对象和方法通常恰好能与现实中的名词和动词对应起来。我们可以凭直觉想出一个带有move方法的Car类,move方法知道如何更新Car的内部状态。OOP鼓励信息隐藏的方式有助于减少耦合。所谓信息隐藏,就是一个对象不会让其他对象访问其内部实现细节,而这些细节是很可能发生变化的。相反,程序的其他部分必须通过对象的公共方法与之进行交互,这些公共方法是更加稳定的接口。

OOP存在两个主要的问题。首先,对于“面向对象”真正意味着什么或者怎么做才是正确的并没有共识。每一种OOP语言和每个程序员的做法都是不一样的。其次,大部分OOP语言都鼓励使用多态和副作用(side  effects,阅读第6章了解更多信息),使得推导、维护和测试代码都变得更加困难,特别是在并发环境下。

2.  函数式编程

函数式编程语言尝试将世间万物用函数的求值来建模。和OOP编程不同,它着重限制了可变数据和副作用的使用。函数式编程关注的是将单纯的函数组合起来,使用一种更加声明式的编程风格(即描述想要实现什么,而不是如何去实现)去构建复杂的代码。这使得函数式编程更容易推导、维护和进行代码测试(6.7节将更深入地讨论这一主题)。流行的函数式编程语言包括Haskell、Lisp系列(即Scheme和Clojure)以及Scala(这是OOP和函数式编程的混合)。

为什么函数式编程没有像OOP那样流行呢?有两个主要原因。一是因为函数式编程的学习曲线比较陡峭。范畴论、monad、monoid、applicative和functor这些概念比“做事情的对象”“猫由动物扩展而来”这些概念更难入门,大众理解起函数式编程的数学根源相对困难;二是因为从设计的角度看,函数式编程并不接地气——它抛弃了副作用和状态,但大多数程序的存在仅仅只是为了维护状态以及能够与外界进行有趣的互动。函数式编程也抛弃了底层硬件架构。例如,它使用递归来代替循环,使用不变数据代替可变数据,使用垃圾回收代替手动内存管理,使用惰性求值(lazy  evaluation)代替迫切求值(eager  evaluation),这些使得函数式代码的性能更加难以预测。有许多方法可以减轻甚至消除函数式编程在性能上的损失,比如使用持久化的数据结构和尾部调用优化(tail  call  optimization),但这些方法通常都会增加程序员的负担。

3.  静态类型

在编程中,每个数据都有它的类型,这决定了数据在内存中是如何存储的,它可以有什么值,我们可以对它执行什么操作。例如,在一些语言里,如果一个值是int,意味它在栈中是以32位有符号二进制补码整数的形式存储的,可能的值从-231到231-1,有效的操作是加、减、乘、除和求余。动态类型语言只会在运行时检查类型,如果尝试访问的数据索引超出了边界就会抛出错误。静态类型语言能够在编译期捕捉确定的类型错误,如果尝试把一个String赋给int,编译的时候就会失败。

静态类型系统就像编译器强制执行的一套自动化测试,随时保证类型是正确的(阅读7.2.1节了解更多信息)。但即便我们使用的是静态类型的语言,也可以(并且应该)编写自动化测试,类型系统可以自动捕捉到大部分bug,从而节省大量时间。静态类型也为阅读代码的人提供了许多有用信息,其中包括开发人员,他们可以把类型签名作为一种文档;还有IDE,可以使用类型签名更容易地实现代码导航、重构和自动编译等功能;而编译器则可以使用类型签名对代码进行优化。

4.  自动内存管理

低级的系统编程语言,比如C和C++,需要程序员手动管理内存的分配和释放。大多数高级编程语言,比如Java、Ruby和Python,都支持自动内存管理,可以让程序员关注于所解决的实际问题,而不是计算机底层的内存架构。这样既提升了生产效率,又能防止很大一部分bug出现,比如忘记释放不再使用的内容(内存泄漏),或者在错误的时间释放内存(迷途指针和双重释放的bug)。

不幸的是,自动内存管理也是有代价的。自动释放内存最常见的方式就是垃圾回收(garbage  collection,GC),即定期运行收集器去扫描所有已分配的内存,对不再使用的内存进行回收。问题就在于收集器的运行会消耗CPU和内存资源,虽然通过调优可以降低这一开销,但对于内存密集型的程序来说开销仍然过多,比如高性能的内存缓存。许多垃圾回收算法还要求在收集期间暂停整个程序,就意味着提供垃圾回收机制的语言不是实时应用程序的好选择。所谓实时应用程序,就是必须在非常短的时间内随时响应的程序(也就是说,比典型的垃圾回收暂停时间更短的周期)。



5.4.2  适用问题


理论上来说,所有现代编程语言都是图灵完备的,所以它们都是等价的。在实践中,一些编程语言解决起某些类型的问题,比用其他语言更方便。例如,有强元编程能力的语言,比如Clojure和Ruby,可以方便地定义出自定义的DSL;Erlang在实现容错的分布式系统方面特别有效;汇编和C通常都是底层、实时或嵌入式系统的唯一选择。

一种语言的社区活跃程度对适用问题也有显著的影响。例如,C++和Python拥有大量的计算机视觉库;Matlab、Mathematica和R语言拥有全面的数学、绘图和统计库;PHP、Ruby、Python、JavaScript和Java有着庞大的生态系统,能提供实现Web应用程序的各种库及框架。对于某些问题领域,选择合适的语言可以带来生产效率上的巨大提升,因为许多代码已经为你编写好了。



5.4.3  性能


编程语言对大多数公司来说通常都不会是瓶颈所在(阅读第7章了解更多信息)。然而,在某些情况下,特别是出现了足够的负载之后,语言就变得很重要了。垃圾回收和并发性是编程语言最常见的两个性能瓶颈。

5.4.1节已经讨论过,垃圾回收会消耗CPU和内存,并且会暂停程序的执行。有一些垃圾回收算法比起其他算法更加成熟、可调整性更高。例如,JVM以拥有更好的垃圾收集器著称,而Ruby  VM的垃圾收集器也因众多性能问题而为人所知。然而,这两种语言在性能方面都无法与没有垃圾回收的语言相比。如果你的应用程序不能容忍任何的GC暂停或者CPU、内存的开销,也许可使用手动内存管理的语言,比如C或C++。

对于并发性,最重要的因素就是一门编程语言支持什么样的并发结构,以及如何处理I/O。例如,Ruby支持线程,但它有全局解释器锁(Global  Interpreter  Lock,GIL),这意味着一次只能执行一个线程。此外,大多数主流的Ruby库执行的是同步I/O,在等待磁盘读取或网络调用返回的时候会阻塞线程。这导致了Ruby并不是处理大量并发的高效语言。目前也有一些解决方案,比如运行多个Ruby进程(即每个CPU内核运行一个)、使用非阻塞的库(例如EventMachine),或者使用不同的VM(例如JRuby),但是这些都需要一定的利弊权衡和开销。

这也是Twitter离开Ruby转向JVM的原因之一。JVM完全支持多线程,没有全局解释器锁,也完全支持非阻塞I/O和多种多样的并发结构,包括线程和锁、Futures、Actor和软件事务内存(Software  Transactional  Memory)。从Ruby到Scala的迁移帮助Twitter将搜索延迟减缓至1/3,CPU使用缩减了一半。



5.4.4  生产效率


虽然编程语言的性能很重要,但对于大多数创业公司来说,程序员的性能才是更大的瓶颈。我们要寻找一门能够以最少时间去完成最多工作的语言。生产效率涉及两个主要方面:有多少现有的代码可以重用,以及你能够创建新代码的速度有多快。

现有代码的数量取决于这门语言的流行程度以及社区的规模。流行的语言拥有更多的学习资源,我们也可以雇到更多已经熟悉该门语言的人,可以使用更多的开源库。成熟语言的生产力工具也是一个生态系统,比如IDE、分析器、静态分析工具和构建系统。我们可以重用的代码越多,需要自己编写和维护的代码就越少。

创建新代码的速度取决于三个因素。第一个因素是经验,你对一门语言的经验越丰富,生产效率就越高,所以要寻找你和团队已经熟悉并且有丰富文档且易于学习的语言。第二个因素是反馈循环,就是代码修改后需要多长时间才能看到效果。如果必须等待几分钟才能完成代码编译和部署,与只需要等待几秒钟让页面刷新或让脚本返回相比,前者的生产效率更低。我们要寻找支持热重载(hot  reload)、具有交互式编码环境(比如“读取  -  求值  -  输出”循环,REPL)、快速编译、快速自动化测试的语言。第三个因素是语言的表达能力,即对于任何给定的想法,需要多少行代码才能实现。需要编写和维护的代码行数越多,面对的bug就越多,前进的速度也越慢(阅读7.2.2节了解更多内容)。一般来说,我们应该在满足其他需求的前提下,尽可能挑选最高级和简洁的语言。



5.4.5  结语


当你选择一门语言时,面对的不仅仅是技术上的权衡取舍,而是一个社区。就像选择一间酒吧一样,没错,你去酒吧是为了品尝美酒,但那还不是最重要的——酒吧更是人们休闲和聊天的地方。这和选择计算机语言的道理是一样的,一门语言随着时间的推移会建立起社区,不仅仅是人,还包括软件方面的产物:工具、库等。这就是为什么有一些语言理论上比其他语言要出色,实际却不如其他语言的原因之一——它们还没有建立起健全的社区。

——Joschua  Bloch,Sun公司分布式工程师、Google首席Java架构师

尽管有数以百计的编程语言可供选择,但足够成熟并且有足够社区支持、可以作为创业公司选项的语言屈指可数。下面是2015年的一份清单,是根据编程语言流行指数(TIOBE、LangPop和RedMonk)、Stack  Overflow开发人员调查和我自己的经验得出的,按字母排序:

·C系列(C、C++、C#)

·Go

·Groovy

·Haskell

·Java

·JavaScript

·Lisp系列(即Clojure或Scheme)

·Perl

·PHP

·Python

·Ruby

·Scala

我们可以应用三个过滤条件快速缩短这一列表:适用问题、编程范式和性能需求。例如,一家做计算机视觉和机器学习系统的创业公司,应该根据适用问题把这份列表限制为三门语言:C++、Java和Python。如果该创业公司更偏爱静态类型,就可以把Python从列表中剔除。最后,如果他们实现的是高性能的实时系统,就不能使用垃圾回收,这样就只剩下了C++。

如果应用了前面这三个过滤条件之后,仍然有多种语言可供选择,我们就可以挑选生产效率最高的语言。例如,一家做Web应用程序的创业公司,最有可能觉得Java、JavaScript、PHP、Python、Ruby和Scala最适合解决他们的问题。如果该团队更喜欢动态类型,可能会从清单中去掉Java和Scala。如果他们中有一小部分人已经熟悉Python,并发现几个Django插件可以节省许多时间,Python将成为他们的最佳选择。