现代编程语言终极测评:概述篇
概述篇:编程语言最重要的特征
一星级篇:C++,JAVA
二星级篇:C#,Python,Rust,TypeScript
三星级篇(上):Go,JavaScript
三星级篇(下):Haskell,OCaml,Scala
四星级篇:Elm,F#
五星级篇:ReasonML,Elixir
你是否想知道:某个编程语言的优缺点是什么?某个编程语言是否能完成我的任务?
通过百度或谷歌等搜索引擎,当你在搜索“最好的编程语言”时,往往会发现各种各样的文章,里面介绍了Python, Java, JavaScript, C#, C++, PHP等编程语言,同时还会拼凑一些让人无法清晰理解的优缺点。
每次看到这种文章,我都会感到十分痛苦,我可以感受到作者的懒惰,毫无经验,并且缺乏想象力和思考能力。
因此,我希望通过这篇文章,跟大家深入讨论和测评现代编程语言,发现各种语言背后的优缺点。
在这篇文章中,我会尝试对较为常见的现代编程语言做出客观且公正的概述与分析,测评排名顺序从一颗星到五颗星(注:半颗星归至一颗星分类)。
值得注意的是,没有一种编程语言能够完美地适用于所有用例。比如,有些语言适合用在前端开发中,有些语言则是和后端或者API编写更加契合,还有些语言适用于系统程序设计。
我还会谈到两种常见的语系:由C语言发展而来的编程语言,以及由元语言(ML)发展而来的编程语言。
编程语言并不仅仅是开发者工具箱中的一个普通工具。如果开发者能够根据特定的任务选择合适的语言,会达到事半功倍的效果。希望我的建议能够帮助你选择最适合你完成任务的编程语言。
编程语言最重要的特征是什么?
绝大多数类似的文章都会基于编程语言的受欢迎程度和赚钱潜力来作比较。但事实上,受欢迎程度并不是一个好的评测标准,特别是在编程时。在本文中,我将着重考虑各编程语言的优缺点。
我会用“点赞👍”“点踩👎”或者“一般👌(即不好也不坏)”这三种方式来对各种编程语言的特征评分:
那么,我们现在来看看,除了语言的受欢迎程度之外,在对编程语言测评时,什么特征最为重要?
类型系统
许多人都对类型系统推崇备至,这也是为什么像TypeScript这种类型的语言最近几年会如此受欢迎。我也倾向于同意的是,类型系统可以消除程序的大量错误,并且使得代码重构更加简单。但是,拥有类型系统只是其中一部分要求。
如果一门编程语言拥有类型系统,那么它最好还得要有类型推论。最好的类型系统不需要显式标注函数签名,就能够推断出绝大多数类型。但是,大部分编程语言只提供部分基础的类型推论。
要是一个类型系统能够支持代数数据类型,那也是极好的(之后会详细讨论这一点)。
最厉害的类型系统应该支持泛型的高级类类型(Higher-kinded Type),让我们能够在更高阶的抽象层面上编程。
我们还应该认识到,人们可能把类型系统看得过于重要了。编程语言中的有些特征比静态类型更加重要。因此,选择一门编程语言时,不应该把是否拥有系统类型作为唯一标准。
学习时需要付出的代价
假设世界上存在一门完美的编程语言,但如果新手开发者要花数月甚至数年的时间去入门和学习,那么这门语言学来又有什么用呢?另外值得注意的是,一些编程范式甚至需要花费数年时间去完全掌握。
因此,一门好的编程语言应该对于初学者来说,是需要容易上手的,并且不需要花费几年时间才能掌握。
空值
我把它称之为价值亿万的错误。它是1965年时,我使用空引用造成的产物。当时,我正在设计第一个为引用制作的全面类型系统,它是使用面向对象语言编写的。我想要确保的是,在编译器自动检查的情况下,所有引用的使用都是安全的。但是我引入了空引用,这造成了数以万计的错误、漏洞以及系统崩溃,并在过去四十年中可能导致了一亿万美元的损失。
——空引用发明者、图灵奖得主托尼·霍尔(Tony Hoare)
空引用之所以不好,是因为它会破坏类型系统。当null是一个默认值时,我们就不能再依靠编译器来检查代码的可行性了。任何空值都是等待着点燃的炮弹。有时候,如果我们认为一个值不为空,并直接使用它(但实际上它就是一个空值),在这种情况下,就会出现运行异常。
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
capitalize("john"); // -> "John"
capitalize(null); // Uncaught TypeError: Cannot read property 'charAt' of null
为了确保我们想要处理的值不为空,我们必须手动进行运行时检测。即使是静态类型语言,空引用也会减弱其类型系统的优势。
function capitalize(string) {
if (string == null) throw "string is required";
return string.charAt(0).toUpperCase() + string.slice(1);
}
事实上,运行时检测(也称作null守卫)是针对不好的编程语言设计的临时解决方案,它会让我们的代码样板化。最糟糕的事情是,我们并不能时刻记得去检测空值。
对于一门好的语言来说,检测值是否存在的类型检测,应该在编译时进行。
拥有另外的缺失值处理机制的语言,应该获得更高排名。
错误处理
捕获异常是很糟糕的处理错误的方式。只有当程序崩溃,并且没办法恢复的异常情况下,我们最好才可以抛出异常。像空值一样,异常会破坏类型系统。
当异常被用作处理错误的主要方式时,我们不可能知道一个函数能不能成功返回一个值。毕竟,如果它运行过程中存在错误,那么异常就会抛出。会抛出异常的函数也是不可能分解的。
function fetchAllComments(userId) {
const user = fetchUser(userId); // may throw
const posts = fetchPosts(user); // may throw
return posts // posts may be null, which again may cause an exception
.map(post => post.comments)
.flat();
}
当然,如果仅仅因为我们不能存取数据,就导致整个应用崩溃的话,这也是绝对不行的。但事实上,这种情况却普遍存在。
因此,我们可以选择手动检查是否存在异常,但这种方法并没有保证,因为我们可能会忘记进行这个操作。同时,这个操作还会增加许多干扰信息:
function fetchAllComments(userId) {
try {
const user = fetchUser(userId);
const posts = fetchPosts(user);
return posts
.map(post => post.comments)
.flat();
} catch {
return [];
}
}
如今,已经有很多更好的错误处理机制。在这些机制中,应在编译时就进行类型检查,发现潜在的错误。非默认使用异常的编程语言会获得更高的排名。
并发性
摩尔定律已经走到了尽头,处理器不能再变快了。我们生活在多核CPU的时代,这就意味着,现代应用必须能利用多核的优点。
但遗憾的是,目前使用的绝大多数编程语言都是在单核计算时代设计出来的,所以,它们并不能很好适应多核。
并发库是事后添加给语言的,最开始的语言设计中并没有考虑并发性,这些库就像轻轻贴上的创可贴。可想而知,这样的设计不能带来友好的开发体验。在一门现代编程语言中,内置的并发性支持是必须考虑的因素(比如Go/Erlang/Elixir)。
不可变性
规模大的面向对象程序会面临着复杂度越来越大的问题,因为我们需要对变换的对象建立大量的对象图。需要去理解和记住的是,当你要调用一个方法时,会发生什么,又会产生哪些副作用。
— Clojure语言创建者里奇·希基(Rich Hickey)
使用不可变值来编程的方法变得越来越受欢迎了,甚至一些现代UI库中(比如React)都开始倾向于使用不可变量。因为不可变性能够消除代码中的一大类bug,所以我认为,对不可变数据值提供一流支持的语言应该获得更高排位。
那么,不可变状态是指的什么呢?简单来说,就是不能被更改的数据(比如大多数编程语言中的字符串)。举例来讲,将字符串改为大写会返回一个新的字符串,而原来的字符串并不会被改变。
不可变性则是进一步发展了这个思想,以此确保事物的不变性。比如说,当我们修改用户名时,在程序中会产生一个全新的、带有更新后用户名的用户对象,而原始的用户对象也会被完全保留。
在不可变状态下,没有东西是共享的。因此,我们也不用担心线程安全的复杂性。不可变性让我们的代码更加容易并行化。
不改变任何状态的函数被称作纯函数,它是易于测试和推论的。当使用纯函数时,我们不用为函数外的事情操心,我们只需要专注于正在使用的函数即可。相比需要记住一整个对象图的OOP来说,这种设计的便利性可想而知。
生态系统/工具
一门语言如果拥有一个大生态系统,那么它就足够吸引人了。如果开发时能够获取到优质的库,那么整个过程就会事半功倍。
我们可以在JavaScript,Python等编程语言中看到这样的特性。
速度
一门语言编译时能够做到多快呢?程序启动速度有多快呢?运行性能又是怎样的?这些都是我会在语言排名中考虑的问题。
时代性
尽管有时会出现意外,但是总的来说,越新推出的编程语言越好,因为新推出的语言会修正旧编程语言中不好的设计,从而变得更加完美。
延伸阅读:
译者:俊一