解决现代编程语言选择难题:命令式编程指南

发表时间: 2021-03-29 09:32

如果搜索“最佳编程语言”,结果会罗列一堆文章。这些文章涵盖各主流语言,并且大多对各语言优缺点的表述模棱两可,表述不到位,缺少实战借鉴意义。本文概述了当前在用的现代编程语言,按推荐程度从低到高依次列出。希望本文有助于读者选择合适的工具完成工作,降低开发工作量。原文篇幅过长。译文按设计用于命令式编程的 C 语言家族,以及设计用于响应式编程的 ML 语言家族,分为上下两篇提供。本文是上篇。


如何了解某种编程语言的优缺点?某种编程语言是否适用于我的项目?面对此类问题,如果求助于搜索引擎,输入“最佳编程语言”,结果会罗列一堆文章,涵盖 Python、Java、JavaScript、C#、C++、PHP 等,并且大多对各语言的优缺点表述得模棱两可。我本人很不喜欢去读此类文章,因为其中许多部分内容的表述并不到位,缺少实战借鉴意义,而且行文生硬。因此我试撰本文来做一次深入总结,去伪存真。


本文概述了当前广为使用乃至可能小众的现代编程语言,力图做到尽量客观、值得一读,并不带个人偏见。各语言按推荐程度从低到高依次列出。


谨记,并不存在所谓完美的编程语言。有的语言非常适用于后端/API 开发,而有的语言则非常适用于系统编程。


文中按当今两大通用语言家族分类,即C衍生语言家族和元语言(Meta Language,ML)衍生语言家族。


对于开发人员而言,编程语言只是工具箱中的工具,更重要的是如何选择合适的工具去完成工作。我衷心希望本文有助于读者选取适合自身项目的编程语言。做出正确的选择,可降低数月甚至数年的开发工作量。

哪些编程语言特性值得关注?



很多编程语言排行榜文章,主要对比的是语言使用广泛度、开发人员收入期望值等因素。在软件领域,虽然大规模的社区和生态系统的确有所裨益,但语言的使用情况并非好的排行指标。本文另辟蹊径,采用的评判依据主要考虑语言的强大之处和不足之处。


为表示所列语言的推荐程度,文中使用“赞”()、“否”()和“尚可”(,即不赞也不否)三种 emoji。


那么应该比较哪些特性?换句话说,除了语言使用广泛性,还有哪些特性更能代表语言的受欢迎程度?

类型系统(Type System)

类型系统倍受大量开发人员的青睐,这也是为什么 TypeScript 之类的语言日渐大行其道。在我看来,类型系统去除了大量的程序错误,更容易实现重构。但是否具有类型系统,只是本文考虑的部分评判因素。


支持类型系统的编程语言,最好同时具备类型推断(type inference)。一个好的类型系统,不用明确地标出函数签名(function signature),也能支持对大部分类型的推断。不幸的是,大多数编程语言提供的仅是基本的类型推断功能。


更进一步,支持代数数据类型(Algebraic Data Types,ADT)的类型系统评分更高。


强大的类型系统,还应支持高级类类型(higher-kinded types)。高级类类型是对泛型(generics)的更高阶抽象,支持编程人员在更高的抽象层上编程。


尽管大家对类型系统寄予厚望,但还有一些比静态类型(static typing)更重要的特性。因此在选择一门编程语言时,不能只看是否支持类型系统,

学习难度

即便编程语言是完美无瑕的,如果一位新手上船需要前期投入数月甚至是数年的精力,那么又会有多少人使用呢?另一方面,很多编程范式需要数年的时间才能逐渐完善。


好的编程语言需对新手友好,掌握它们不应花费大量学习时间。

空值

我将 1965 年创建的空值引用(null reference)称为“亿万美元错误”。当时,我正设计首个完全类型系统,用于面向对象语言中的引用。目标是确保所有对引用的使用是绝对安全的,并由编译器自动执行检查。我无法克制添加空值引用的诱惑,完全因为空值引用非常易于实现。近四十年来,这一设计导致了不计其数的错误、漏洞和系统崩溃,可能造成了数十亿美元的痛心损失。


— 空值引用的创立者 Tony Hoare


为什么说空值引用是不好的?因为空值引用破坏了类型系统。一旦默认为空值,那么就不能依靠编译器检查代码的有效性。任何空值都是一枚随时可能引爆的炸弹。如果没能想到所使用的值的确为空值,那么会产生什么后果?会出现运行时错误。


function capitalize(string) {  return string.charAt(0).toUpperCase() + string.slice(1);}capitalize("john");  // -> "John"capitalize(null);   // 未捕获类型错误:不能读取为空值的属性“charAt”。

复制代码

为确保所处理的值并非空值,开发人员必须对运行时做手工检查。即使是静态类型语言,空值引用也破坏了类型系统的很多优点。

function capitalize(string) {  if (string == null) throw "string is required";      return string.charAt(0).toUpperCase() + string.slice(1);}

复制代码


运行时检查也称为“空值防护”(null guards),在现实中可归为一种不良的编程语言设计。一方面,引入样板代码破坏了编程风格。更糟的是,它并不能确保我们是否检查了空值。


好的编程语言,应在编译时做类型检查,判断值的存在与否。


因此,支持空值检查机制的编程语言应加分。

错误处理

捕获异常并不是一种好的错误处理方式。抛出异常本身没有问题,但仅适用于程序没有办法恢复而必须崩溃这类异常情况。异常和空值一样,会破坏类型系统。


如果将异常作为错误处理的首选方式,那么就无法获知函数是返回了期望值,还是发生了故障。抛出异常的函数也无法实现复合(Compose)。


function fetchAllComments(userId) {  const user = fetchUser(userId); // 可能抛出异常。    const posts = fetchPosts(user); // 可能抛出异常    return posts    // posts可能为空值,这会再次导致异常。          .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 等。

不可变性

我认为大型的面向对象程序,需要解决由于大规模可变对象间关联所导致的复杂图结构。否则在调用方法时,必须得把握并牢记该方法的功能和副作用。


—— Rich Hickey,Clojure 创建者。

当前的编程工作中,使用不可变值越来越常见。即便是 React 这样的现代 UI 软件库,也考虑使用不可变值。对支持不可变数值提供一等支持的编程语言,我们会给出更高的评判。这完全是因为不可变性避免了编程中出现许多软件缺陷。


什么是不可变状态?简而言之,就是数据不会发生改变。例如,大多数编程语言中的字符串。字符串转为大写,并不会去改变原始的字符串,而是返回一个新的字符串。


为确保任何事情都不发生改变,不可变性对上述理念做了进一步扩展。更改不可变数组,总是会返回一个新的数组,而非原始数组。更新用户名,将返回一个包含更新后用户名的新用户对象,并不改变原始对象。


不可变状态不做任何共享,因此无需操心线程安全所导致的复杂性。不可变性使得代码更易于并行化。


不对状态做任何更改的函数,称为“纯函数”(Pure)。纯函数更易于测试和推断。使用纯函数,无需操心函数体之外事情,可聚焦于函数本身。不用像面向对象编程中那样必须牢记整个对象图,这样极大地简化了编程开发。

生态系统和工具链

一种编程语言可能本身并没有多少亮点,但如果其具有大型的生态系统,这会令语言更具吸引力。具备良好的软件库,可以节省数月乃至数年的开发工作。


显著的例子就是 JavaScript 和 Python。

速度

语言的编译速度如何?程序的启动速度如何?运行时的性能如何?所有这些都是影响评判中的考虑因素。

诞生年代

尽管并非绝对,通常新推出的语言要比原先的语言更好。只是因为新语言会吸取了前辈的经验教训。

C++



下面从最糟糕、也可能是计算机科学中最大错误的 C++语言开始。当然,我并不认为 C++是一种很好的现代编程语言。但 C++当前依然得到广泛应用,在此必须提及。


语言家族:C


语言特性



C++可称为糟糕透顶的语言……如果项目局限于 C,意味着不会有任何机会被 C++愚蠢的“对象模型”搞砸。


—— Linux 创立者 Linus Torvalds

C++中填充了各种特性,力图无所不能,但在在其中任何一项上都不能说出色。C++支持 goto、指针、引用、面向对象编程、操作符重载,以及各种非生产特性。


为什么说 C++不好?在我看来,最大问题在于 C++颇具年头了。C++是在 1979 年设计的。在当时设计者缺少经验,关注点发散,虽然所添加的特性在当时看来是似乎好的做法。C++得到了非常广泛的使用,这意味着为其中支持各种用例而添加了更多特性,导致特性成堆。


速度

C++的编译时间出奇的慢,甚至比 Java 慢很多,尽管与 Scala 不相上下。


但在运行时性能和启动时间上,C++程序表现非常优秀。


生态系统和工具



上图的推文给出了很好的解释。C++编译器的错误信息对新手并不友好。通常并未指出导致错误的确切原因,需要开发人员花时间查找。


垃圾回收


我曾希望在 C++0x 标准中至少考虑可选地支持垃圾回收,但这在技术上存在问题。


—— C++的创建者 Bjarne Stroustrup

垃圾回收从未添加到 C++中,而手工内存管理非常易于出错。开发人员必须操心如何手工释放和分配内存。我对使用非垃圾回收语言的经历记忆深刻,其中大量的缺陷在当前支持垃圾回收语言中可轻易避免。


面向对象编程的失败尝试


我提出了“面向对象”一词,但并没有没有顾及 C++。


—— 面向对象编程的创建者 Alan Kay


面向对象编程是一项很好的技术,出现于上世纪六十年代后期,当时 C++刚出现。不幸的是,不同于 Smalltalk 等语言,C++在实现面向对象编程中出现了几个致命错误,导致好的理念变成噩梦。


好的一方面是,不同于 Java,至少在 C++中面向对象是可选的。


学习难度



C++是一种复杂的低层(low level)语言,不具备任何自动内存管理机制。由于特性纷杂,初学者必须花费大量时间学习。


并发

C++设计用于单核计算时代,只支持简单的并发机制,这还是在近十年中添加的。


错误处理

抛出并捕获错误是 C++的首选错误处理机制。


不可变性

未内置对不可变数据结构的支持。


空值

C++中所有引用均可为空值。

评判



C++的初衷是成为更好的 C 语言,但这一初衷并未实现。


系统编程是 C++的最适合使用场景。但考虑到已具有 Rust 和 Go 等更好、更现代的替代语言,系统完全可以不用 C++实现。不管读者同意与否,我不认为 C++具有任何优点。


是该终结 C++的时候了。

Java



Java 是自 MS-DOS 以来计算机领域中最令人困扰的事情。


—— 面向对象编程创始人Alan Kay


Java 出现在 1995 年,比 C++晚了 16 年。Java 是更简单的编程语言,由此得到广泛使用。


语言家族:C。


垃圾回收

相比 C++,Java 的最大优点是具有垃圾回收,这极大地消除了各类软件缺陷。


生态系统

Java 已经存在很长时间,在后端开发领域形成了大型生态系统,极大地降低了开发负担。


面向对象语言

本文不会深入探讨面向对象编程的不足。详细分析可阅读本文作者的另一篇文章,“面向对象编程:亿万美元灾难”。


在此给出计算机科学中一些最为杰出人士的看法:


抱歉,我多年前使用了“对象”一词。该词使得很多人聚焦于一个更狭义的理念,虽然更广义的理念是消息传递。


—— 面向对象编程的创始人 Alan Kay


Alan Kay 是对的,许多主流面向对象编程语言并未找准关注点。它们聚焦于类和对象,而忽视了消息传递。幸运的是,Erlang 和 Elixir 等一些现代编程语言找准了方向。


受面向对象编程影响的编程语言,会导致计算机软件冗长、可读性不好、描述性差、难修改和维护。


—— Richard Mansfield


所有使用 Java、C#等面向对象编程语言的开发人员,如果曾具有使用非面向对象编程语言的经验,对此应深有体会。


速度

大家都知道,Java 运行在 JVM 之上,而 JVM 的启动速度是出名的慢。我曾看到有运行在 JVM 上的程序耗时 30 多秒才启动起来。对于现代云原生程序,这是不可接受的。


一个大型项目,如果编译速度慢,就会对开发人员的生产效率产生显著影响。Java、Scala 等 JVM 语言存在同样的问题。


但从好的一面说,JVM Runtime 的性能还算不错。


学习难度

尽管 Java 是一种相当简单的语言,但 Java 以面向对象编程为主,这使得 Java 很难做到优秀。编写一个简单的 Java 程序可信手拈来,但是掌握如何编写可靠、可维护的面向对象代码,则需要十数年的 Java 功力。


并发

Java 设计于单核计算时代,和 C++一样,仅支持基本的并发特性。


空值

Java 中,所有引用均可为空值。


错误处理

抛出并捕获错误是 Java 的首选错误处理机制。


不可变性

未内置对不可变数据结构的支持。

判定



Java 在刚推出时,的确是一种很好的编程语言。但遗憾的是不同于 Scala 等语言,Java 始终专注于面向对象编程。 Java 编程严重受模板代码的影响,冗余代码多。


Java 应该退居二线了。

C#



C#和 Java 并没有本质上的差异。C#的早期版本,就是微软的 Java 实现。


C#具有 Java 的大部分优点。C#于 2000 年推出,比 Java 晚 5 年,借鉴了 Java 的经验教训。


语言家族:C


语法

C#在语法上一直保持略微领先 Java。尽管是一种面向对象语言,但 C#在解决模板代码问题上比 Java 有所改进。很高兴看到 C#每个新版本都能改进语法。例如,添加了表达体函数成员(expression-bodied function members)、模式匹配、元组等特性。


面向对象语言

和 Java 一样,C#主要针对面向对象编程。面向对象编程的缺点如上所列,在此不再详述。下面列出一些知名人士的观点。


我认为相比函数式语言,面向对象语言中缺失可重用性。问题在于,面向对象语言需要处理其所提供的所有隐含(implicit)环境。尽管我们想要的只是一根香蕉,但却得到了一只握着香蕉的大猩猩,甚至是整个丛林。


—— Erlang 的创建者 Joe Armstrong


我完全同意这个说法,相比函数式编程,命令式编程非常难以重用面向对象代码。


面向对象编程提供了对正确做法的一个反面教材……


—— 计算机科学先驱 Edsger W. Dijkstra


从我自己使用面向对象和非面向对象编程的经验看,我完全同意面向对象代码更难以正确实现功能。


多范式(Multi-paradigm)

C#声称是一种多范式语言,尤其是声称支持函数式编程,但我并不同意。对函数提供一流支持(first-class functions),并不足以称之为函数式语言。


那么什么语言可称为具备函数式特性?应至少内置支持不可变数据结构、模式识别、组合函数的管道操作符、代数数据类型(ADT)等特性。


并发

和 Java 一样,C#创立于单核计算时代,仅提供基本的并发支持。


空值 Nulls

C#中,所有引用均可为空。


错误处理

抛出并捕获错误是 C#的首选错误处理机制。


不可变性

未内置对不可变数据结构的支持。

评判

尽管我本人的职业生涯中主要使用的是 C#,但还是对这种语言评价不高。与对 Java 的评判一样,我建议读者寻找更现代的替代语言。C#在本质上依然是 Java,只是具有更现代的语法。

不幸的是,C#本身并不“sharp”。

Python



Python 早在 1991 年提出,和 JavaScript 并称当前使用最广的两种语言。


语言家族:C


生态系统

Python 软件库几乎无所不能。不同于 JavaScript,Python 不能用于 Web 前端开发,但大规模的数据科学软件库弥补了这方面的不足。


学习难度

Python 语言非常简单,初学者数周就能上手。


类型系统

Python 是动态类型的,因此谈不上需要类型系统。


速度

Python 是一种解释性语言,性能慢。对性能有严格要求的程序,可使用 Cython 替代原生的 Python。


相对于原生语言,Python 的启动也相当慢。


工具

对比其他的现代编程语言,难免会对 Python 的依赖管理颇为失望。目前存在 pip、pipenv、virtualenv、pip freeze 等工具。相比之下,JavaScript 只需要 NPM 这一种工具。


并发

Python 在创建时并未全面考虑并发,仅提供基本的并发特性。


空值

Python 中所有引用均可为空。


错误处理

抛出并捕获错误是 Python 的首选错误处理机制。


不可变性

未内置对不可变数据结构的支持。

评判



很不幸,Python 并不提供对函数式编程的支持。函数式编程非常适合处理数据科学所面对的问题。即便是在 Python 擅长的 Web 爬虫领域,Elixir 等函数式语言表现更好。


我并不推荐使用 Python 完成大型项目,该语言在构建中并未充分地考虑软件工程。


如果有更好的选择,不推荐在数据科学之外使用 Python。在数据科学领域,Julia 可能是 Python 的很好替代,尽管相比 Python 而言,Julia 的生态系统近乎不存在。

Rust



Rust 是一种现代低层语言,最初设计用于替代 C++。


语言家族:C


速度

运行快速是 Rust 设计所秉持的初衷。在编译性能上,Rust 程序要慢于 Go 程序,但运行时性能比 Go 稍快。


空值

至此,本文推荐列表中终于出现支持现代空值的语言了。Rust 中没有 null 或 nil 值,开发人员使用 Option 模式。


// 源代码: https://doc.rust-lang.org/rust-by-example/std/option.html// 返回值或者是T类型的Some,或是None。enum Option<T> {    Some(T),    None,}// 整数除法不会出错。fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {    if divisor == 0 {        // 错误表示为None。        None    } else {        // 结果使用Some封装。        Some(dividend / divisor)    }}// 该函数用于处理失败的除操作。fn try_division(dividend: i32, divisor: i32) {    // 与其他枚举一样,Option值可模式匹配。    match checked_division(dividend, divisor) {        None => println!("{} / {} failed!", dividend, divisor),        Some(quotient) => {            println!("{} / {} = {}", dividend, divisor, quotient)        },    }

复制代码


错误处理

Rust 的错误处理引入了现代函数式方法,使用特定的 Result 类型,声明可能会产生失败的操作。Result 模式非常类似于 Option 模式,只是在 None 的情况下依然有值。


// 结果或者是T类型的OK函数值,或是E类型的Err函数值。enum Result<T,E> {    Ok(T),    Err(E),}// 存在失败可能的函数。fn random() -> Result<i32, String> {    let mut generator = rand::thread_rng();    let number = generator.gen_range(0, 1000);    if number <= 500 {        Ok(number)    } else {        Err(String::from(number.to_string() + " should be less than 500"))    }}// 处理函数的结果。match random() {    Ok(i) => i.to_string(),    Err(e) => e,

复制代码


内存管理

在本文列出的现代编程语言中,Rust 是唯一不提供垃圾回收的。Rust 迫使开发人员去考虑如何实现底层的内存管理,这影响了开发人员的效率。


并发

由于 Rust 中缺少垃圾回收,因此实现并发是相当困难的。开发人员必须考虑“装箱”(boxing)和“钉住”(Pinning)。这在具有垃圾回收机制的语言中,通常是自动完成的。


不可变性

未内置对不可变数据结构的支持。


低层语言

作为一种低层语言,开发人员的生产效率无法其他高层语言相比。同时,语言的学习难度明显增大。

评判



Rust 非常适合系统编程。尽管比 Go 更复杂,但 Rust 提供了强大的类型系统。Rust 提供了现代的空值替换和错误处理方法。


为什么本文将 Rust 排在 TypeScript 和 JavaScript 之后?Rust 是一种设计用于系统编程的低层语言,并非后端和 Web API 开发的最适合选项。Rust 缺少垃圾回收机制,未内置对不可变数据结构的支持。

TypeScript



TypeScript 语言编译为 JavaScript,通过对 JavaScript 添加静态类型,意在成为一种“更好的 JavaScript”。类似于 JavaScript,TypeScript 同样用于前端和后端开发


TypeScript 由同是 C#设计者的 Anders Hejlsberg 设计的,因此代码看上去非常类似 C#,可认为是一种用于浏览器的 C#。


语言家族:C。


JavaScript 的超集

TypeScript 将自己定位为 JavaScript 的超集,这有助于人们采用。毕竟大多数人对 JavaScript 耳熟能详。


但作为 JavaScript 的超集,更多程度上是一种缺点。这意味着 TypeScript 继承了 JavaScript 的全部问题,局限于 JavaScript 所有的不良设计决策。


例如,应该没有开发人员喜欢 this 关键词吧。但 TypeScript 依然刻意原封照搬。


再有,其类型系统时常令人感到奇怪。


[] == ![];    // -> 为真NaN === NaN;  // -> 为假!

复制代码


换句话说,TypeScript 具有 JavaScript 的所有缺点。一种糟糕语言的超集并不会变身成为一种优秀的语言。


生态系统

TypeScript 完全分享了 JavaScript 庞大的生态系统。这是其最大优点。特别是相比 Python 等语言,NPM 非常好用。


缺点在于,并非所有的 JavaScript 软件库都可在 TypeScript 中使用,例如 Rambda/Immutable.js 等。


类型系统

个人感觉,TypeScript 的类型系统毫无亮点。


好的一面是甚至提供对 ADT 的支持。例如下面给出的差别联合(discriminated union)类型:

// 源代码来自https://stackoverflow.com/questions/33915459/algebraic-data-types-in-typescriptinterface Square {    kind: "square";    size: number;}interface Rectangle {    kind: "rectangle";    width: number;    height: number;}interface Circle {    kind: "circle";    radius: number;}type Shape = Square | Rectangle | Circle;function area(s: Shape) {    switch (s.kind) {        case "square": return s.size * s.size;        case "rectangle": return s.height * s.width;        case "circle": return Math.PI * s.radius ** 2;   

复制代码


下面是使用 ReasonML 实现的同样代码:


type shape =    | Square(int)   | Rectangle(int, int)   | Circle(int);let area = fun   | Square(size) => size * size   | Rectangle(width, height) => width * height   | Circle(radius) => 2 * pi * radius;

复制代码


差别联合类型是在 TypeScript 2.0 中增添的,TypeScript 的语法尚未企及函数式语言的高度。例如,在 switch 中的字符串匹配易于出错,编译器无法在大小写错误时给出警告。


TypeScript 仅提供基本的类型推断。此外在使用 TypeScript 时,any 关键字的出现频次难免过高。


空值

TypeScript 2.0 添加了对不可为空(non-nullable)类型的支持,使用编译器选项--strictNullChecks 启用。但使用不可为空类型并非编程默认,也并非 TypeScript 的惯用做法。


错误处理

TypeScript 中,使用抛出和捕获异常处理错误。


新的 JavaScript 特性

新酷特性首先在 JavaScript 中得到支持,然后才是 TypeScript。实验特性可使用 Babel 在 JavaScript 中得到支持,而在 TypeScript 中则无此功能。


不可变性

TypeScript 对不可变数据结构的处理,要显著劣于 JavaScript。JavaScript 开发人员可使用支持不可变性处理的软件库,但 TypeScript 开发人员通常必须依赖原始数组或对象展开操作符(spread operator),即写入时复制(copy-on-write)。


const oldArray = [1, 2];const newArray = [...oldArray, 3];const oldPerson = {   name: {     first: "John",     last: "Snow"   },   age: 30};// 执行对象深拷贝(deep copy)非常繁琐。const newPerson = {  ...oldPerson,  name: {     ...oldPerson.name,     first: "Jon"  }

复制代码


正如上面代码所示,原生扩展操作符并不支持深拷贝(deep copy),而手工扩展深度对象非常繁琐。大型数组和对象的拷贝的性能也非常不好。


但 TypeScript 中,readonly 关键字非常好用,用于定义属性是不可变的。虽然如此,TypeScript 要对不可变数据结构提供很好支持,依然需要很多工作。


JavaScript 提供了一些操作不可变数据的很好软件库,例如 Rambda/Immutable.js。但是,实现此类软件库对 TypeScript 的支持并非易事。


TypeScript 对比 React


相比Clojure等从设计上考虑到不可变数据处理的语言,在 JavaScript 和 TypeScript 中不可变数据的处理相对更为困难。


—— 原文引用自React官方文档


继续说缺点。前端 Web 开发推荐使用 React。


React 并未针对 TypeScript 设计。最初,React 是针对函数式语言设计的,本文稍后会详细介绍。二者在编程范式上存在冲突,TypeScript 是面向对象编程优先的,而 React 是函数优先的。


React 中,函数参数 props 是不可变的;而 TypeScript 中,没有内置提供适用的不可变数据结构支持。


在开发中,TypeScript 相比 JavaScript、React 的唯一优点是,无需操心 PropTypes。

TypeScript 是否是 JavaScript 的超集?

这取决于开发人员的认识。至少我认为是的。做为超集的最大优点,是可接入整个 JavaScript 生态系统。


为什么 JavaScript 的超集语言备受关注?这与 Java、C#广为采用是同样的原因,是因为背后有市场营销预算充足的大厂在提供支持。

评判



尽管 TypeScript 常被认为是“更好的 JavaScript”,但我依然评判其劣于 JavaScript。TypeScript 相比 JavaScript 的优点被夸大了,尤其是对于使用 React 做前端 Web 开发。


TypeScript 保留了 JavaScript 的所有不足,实际上也继承了 JavaScript 中数十年积累不良设计决策,的确并非一种成功的交付,

Go



Go 设计上主要考虑了提高多核处理器和大规模代码库的编程效率。Go 的设计者们当时任职于谷歌,因对 C++的共同不喜而得到灵感。


语言家族:C。


并发

并发是 Go 的杀手级特性。Go 从本质上就是为并发而构建。和 Erlang/Elixir 一样,Go 使用邮箱模型(Mailbox)实现并发。不幸的是,goroutines 并未提供 Erlang/Elixir 进程那样的统一容错特性。换句话说,goroutine 中的异常将导致整个程序宕机,而 Elixir 进程中的异常只会导致当前进程终止。


速度

编译速度是谷歌创建 Go 的一个重要考虑。有个笑话,谷歌利用 C++编译代码的时间就创建出了 Go。


Go 是一种高效的语言。Go 程序的启动时间非常快。Go 编译为原生代码,所以运行时速度也非常快。


学习难度

Go 是一种简单的语言,如果得到有经验前辈的指导,新手能在一个月内掌握。


错误处理

Go 并不支持异常,由开发人员显式处理各种可能的错误。和 Rust 类似,Go 也返回两个值,一个是调用的结果,另一个是可能的错误值。如果一切运行正常,返回的错误值是 nil。


不支持面向对象编程


虽然这么说有人会反对,但我个人认为,不支持面向对象特性是很大的优势。


重申 Linux Torvalds 的观点:


C++是一种很糟的(面向对象)语言……将项目局限于 C,意味着整个项目不会因为任何愚蠢的 C++“对象模型”而搞砸。


—— Linux 创建者 Linus Torvalds


Linus Torvalds 公开对 C++和面向对象编程持批评态度。限制编程人员在可选的范围内,是他完全正确的一面。事实上,编程人员的选择越少,代码也会更稳定。


在我看来,Go 可以回避了许多面向对象特性,免于重蹈 C++的覆辙。


生态系统


一些标准库的确很笨重。大部分并不符合 Go 返回带外(out-of-band,OOB)错误的自身哲学。例如,有的库对索引返回-1 值,而非(int, error)。还有一些库依赖全局状态,例如 flag 和 net/http。


Go 的软件库缺少标准化。例如在错误时,有的库返回(int, error),也有软件库返回-1 等值。还有一些库依赖标识等全局状态。


Go 的生态系统规模远比不上 JavaScript。


类型系统



几乎所有的现代编程语言都具有某种形式的泛型,其中包括 C#和 Java,甚至是 C++也提供模板类。泛型支持开发人员重用不同类型的函数实现。如果不支持泛型,那么开发人员就必须对整型、双精度和浮点型单独实现加法函数,这将导致大量的代码冗余。换句话说,Go缺失对泛型的支持导致了大量冗余代码。正如有人指出的,“Go”是“去写一些模板代码”(Go write some boilerplate)的缩写。


空值

不幸的是,即使更安全的空值替代方案已存在数十年,Go 依然在语言中添加了空值。


不可变性

未内置对不可变数据结构的支持。

评判



Go 并非一种好的语言,但也谈不上不好,只是不够优秀。使用一种并不优秀的语言时需谨慎,因为这可能会导致我们在随后的二十年中陷入困境。


Will Yager 的博客文章“Why Go Is No Good”


如果你并非供职于谷歌,也没有面对类似谷歌的用例,那么 Go 可能并非好的选择。Go 是一种最适合系统编程的简单语言,但并非 API 开发的好选择。原因是因为我们有更多更好的替代语言,本文稍后介绍。


我认为总体而言,尽管 G 的类型系统略弱,但比 Rust 还是略好。Go 是一种简单的语言,非常快,易于学习,并且具有出色的并发功能。当然,Go 成功地实现了做为“更好的 C++”这一设计目标。

最佳系统编程语言奖



最佳系统语言奖授予 Go。实至名归,Go 是系统编程的理想选择。Go 是一种低层语言,使用 Go 构建的大量成功项目,例如 Kubernetes,Docker 和 Terraform,证明其非常适合系统编程。

JavaScript



作为当前最流行的编程语言,JavaScript 无需过多介绍。


当然,将 JavaScript 排在 Rust、TypeScript 和 Go 之前是正确的。下面给出原因。


语言家族:C


生态系统

生态系统是 JavaScript 的最大优势。我们能想到的所有,,包括 Web 的前端和后端开发,CLI 编程、数据科学,甚至是机器学习,都可使用 JavaScript。JavaScript 可能具有提供任何功能的软件库。


学习难度

JavaScript 和 Python 都是非常容易学习的编程语言。几周就能上手做项目。


类型系统

和 Python 类似,JavaScript 是动态类型的。无需过多解释,但是其类型系统时常看起来很奇怪:


[] == ![] // -> 为真NaN === NaN; // -> 为假[] == ''   // -> 为真[] == 0    // -> 为真

复制代码


不可变性

在 TypeScript 一节中已经介绍,展开操作符(spread operator)会影响性能,甚至并没有在拷贝对象时执行深拷贝。尽管有 Ramda/Immutable.js 等软件库,但 JavaScript 缺少对不可变数据结构的内建支持。


JavaScript 并非针对响应式设计的

在 JavaScript 中使用 React,必须借助 PropTypes。但这也意味着必须去维护 PropTypes,这会导致灾难性后果。


此外,如果在编程中不加注意的话,可能会导致严重的性能问题。例如:


<HugeList options=[] />

复制代码


这个看上去无害的代码会导致严重性能问题。因为在 JavaScript 中, [] != []。上面的代码会导致 HugeList 在每一次更新时重渲染,尽管 options 值并未发生变化。此类问题会不断叠加,直到用户界面最终无法响应。


关键字 this

关键字 this 应该是 JavaScript 中的最大反特性。其行为持续表现不一致,在不同的情况下可能意味完全不同,其行为甚至取决于谁调用了指定的函数。使用 this 关键字通常会导致一些细微而奇怪的错误,难以调试。


并发

JavaScript 使用事件循环支持单线程并发,无需考虑加锁等线程同步机制。尽管 JavaScript 在构建时并未考虑并发性,但与大多数其他语言相比,更易于实现并发代码。


新的 JavaScript 特性

相比 TypeScript,新特性能更快地在 JavaScript 中支持。即便是实验性特性,也可使用 Bable 支持在 JavaScript 中使用。


错误处理 Error handling

抛出并捕获错误是 JavaScript 的首选错误处理机制。

评判



JavaScript 并非一种很好设计的语言。JavaScript 的最初版本仅用十天就拼凑出来,尽管在后期版本中修正了许多缺点。


抛开上述缺点,JavaScript 依然是全栈 Web 开发和很好选择。如果加以适当的代码修炼和分析,JavaScript 是一种很好的语言。