写点什么

一文解决现代编程语言选择困难:命令式编程

2021 年 3 月 28 日

一文解决现代编程语言选择困难:命令式编程

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

interface 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 是一种很好的语言。

 

原文链接: These Modern Programming Languages Will Make You Suffer

2021 年 3 月 28 日 09:003726

评论 5 条评论

发布
用户头像
你懂rust吗?就xjb写?
2021 年 04 月 08 日 14:58
回复
用户头像
Rust
不可变性

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

这个搞错了吧,语法不是直接支持了么?默认不可变,加上mut可变。
2021 年 03 月 29 日 15:27
回复
不会rust,但看文中代码,你说的应该是声明某个引用一旦被赋值,就不能改为引用其他值,而用了mut则声明可以修改该引用指向的值。而“不可变性”通常是指创建了一个对象之后,它的内部数据是不可变的(修改不了了),两者不是一回事。
2021 年 03 月 31 日 16:32
回复
不是,Rust 中如果不声明 mut,那么对应的引用也不能修改,例如:
let my_list : Vec<i32> = Vec::new();
那么 my_list.push(1); 这个编译时就会报错,因为对引用进行了修改,除非改为
let mut my_list : Vec<i32> = Vec::new();
我觉得原文作者可能是想要类似 C# 中 ImmutableList 这种数据结构,但 Rust 中能通过语法来进行约束就没必要存在了。
2021 年 03 月 31 日 16:49
回复
原来如此。你可以点原文链接,跟原作者探讨下。
2021 年 03 月 31 日 17:26
回复
没有更多了
发现更多内容

架构师训练营作业

郎哲

极客大学架构师训练营

java实现一致性 hash 算法

Mars

一致性Hash算法

架构师训练营 - 第 9 周课后作业(1 期)

阿甘

第五周作业

Griffenliu

架构师训练营 - 第九周 - 作业一

行者

第五周总结

Griffenliu

架构师训练营 2 期 - 第五周总结

Geek_no_one

极客大学架构师训练营

顺序查找

ilovealt

算法和数据结构

架构师训练营第九周作业

邓昀垚

极客大学架构师训练营

架构师训练营 2 期 - 第5周命题作业

Geek_no_one

极客大学架构师训练营

性能优化(三)

wing

极客大学架构师训练营

架构师训练营第九周学习总结

文智

极客大学架构师训练营

五周 - 作业

水浴清风

一致性hash

架构师训练营第九周学习笔记

郎哲

极客大学架构师训练营

「架构师训练营」第 5 周作业

小黄鱼

极客大学架构师训练营

架构2期第5周作业

supersky6

第五周笔记

willson

极客大学架构师训练营

架构师训练营第九周作业

Shunyi

极客大学架构师训练营

Python进阶——如何正确使用魔法方法?(上)

Kaito

Python

架构一期 第九周作业

haha

极客大学架构师训练营

第九周作业

TheSRE

极客大学架构师训练营

「架构师训练营第 1 期」第九周作业

张国荣

第 5 周 系统架构总结

心在那片海

架构师训练营第 1 期 -- 第九周作业

发酵的死神

极客大学架构师训练营

第 5 周 系统架构作业

心在那片海

架构师训练营第九周总结

邓昀垚

极客大学架构师训练营

一致性hash算法

落朽

训练营第五周总结

大脸猫

极客大学架构师训练营

第九周总结

solike

架构师训练营 - 作业 - 第九周

Max2012

第九周总结

Geek_ac4080

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

一文解决现代编程语言选择困难:命令式编程-InfoQ