最近在学 rust 。事情的起因是,有人跟我说:“学完 rust 之后你就自动会写安全现代的 cpp 了”。我对于这种能提高编程思维和品味之类的事,由于本就缺乏,所以会更感兴趣。加上那段时间还算清闲,我很快就开始了 rust 的学习。

那么清闲的时光已经离我远去了,我什么时候才能再拥有那样的时光呢,欸。

说实话,我现在已经学了教程的一半,但是由于基础差的太多,所以对那句话我还是不太能理解。这个状态一直持续到我看了一本书,叫做《代码的文明》。不是什么很流行的书,说不定也不是什么很高质量的书,因为我在 zlibrary 上甚至没找到电子版,最后还是买了本实体书(上次买实体书还是在上次了)。主要讲的编程语言的发展史,以及背后蕴含的设计哲学和思想。对于我这种还没有深度使用过多种语言,没有感受过具体形式之上,有关抽象层面的语言差距的小菜鸟来说,这本书还是非常有用的,至少给了我一个思考的方向,带我入了门。

接下来我将结合在学习 rust 过程中的感悟和看那本书的收获,谈一谈我对那句话浅薄的理解——一切的关键都在于所有权——这是 rust 的核心,也是 rust 领先于其他语言的机制优势。但我不会介绍所有权是什么,更偏向于所有权这个机制下,写代码时的一些哲学对于写 cpp 的帮助。

可变与不可变

在 rust 中,有两个和可变与不可变有关的机制:

  1. 变量默认是不可变的,如果你需要它可变,则在初始化时需要加上 mut 。
  2. 在 rust 中,你只能同时拥有一个可变引用,或者多个不可变引用,同时拥有多个可变引用和同时拥有可变和不可变引用都是不被允许的。

引用,让你可以“借用”某个值而不获取其所有权。

这两个机制事实上让代码变得更难以编写,但能让写出来的代码更符合你的想法去工作。有时候,我们并没有理清变量什么时候应该怎么变,在 cpp 没那么严格的限制下,我们可以迅速写出一个貌似正确的代码,这做上去很简单,但一旦我们开始运行,会发现变量没按我们预想的改变。写 rust 的时候,语言本身的特性会强制你提前考虑好这些内容, rust 的思维方式也能让你更好理清变量的所有行为。所以如果将 rust 的书写思维运用到我们写 cpp 中,我们会更频繁地使用 const ——这个大部分初学者在写 cpp 时觉得累赘的东西,相关的错误会大大减少,也让后续代码的维护和 debug 更加易于进行。

异常处理

在 rust 中,异常分为可恢复错误和不可恢复错误。可恢复错误用 Result<T, E> 枚举作为函数的返回值。不可恢复错误用 panic! 宏触发栈展开或直接中止。在 cpp 中则没有严格的区分,但是显然的是,对异常做区分让程序更健壮,当异常可恢复时程序并不会崩溃,也让程序员对自己编写的程序有更深刻的理解。很多时候我们对自己写出来的程序理解是不够的( vibe coding 时代这个问题更甚),我们知道这样写能成功,但是知道不了成功的所有细节是怎样的。区分可恢复和不可恢复异常,就是将我们的视角更多的投射到了这上面,只有清楚细节是怎样的,才能判断可恢复与否。

在异常问题上, cpp 和 rust 还有很大的一点不同是, rust 强制你显式处理异常,而 cpp 则靠程序员自觉。我当然赞同,程序员应该保证抛出的异常都得到了捕获,但很多时候都不是这样。 rust 的 Result<T, E> 枚举天然让你穷尽所有异常,在设计和编写代码时,就会下意识的去考虑所有异常的处理,这有利于我们在写 cpp 时,将所有抛出的异常都捕获。

cpp 的异常处理哲学,适合那些真正的错误,而 rust 则让代码里所有不符合你目的的行为都能够被你看见和处理。两者在这上面的差别体现出来的哲学差异,多少也反映了两门语言的设计哲学差异,此处先不展开。但我认为,对于小白来说,用 rust 锻炼好自己的思维之后,再去写 cpp ,哪怕你不严格按照 rust 思维去写,也能帮助你避开很多坑。而且,想得到但不去做,和想不到所以没做,两者可谓天差地别。

No NULL

相信很多大佬知道这个事,就当我给和我一样的小白科普一下了,空引用的发明者,图灵奖得主 Tony Hoare 曾说过:“我称之为我的十亿美元错误。那就是 1965 年引入的空引用。当时,我正在为面向对象语言(ALGOL W)设计第一个全面的引用类型系统。我的目标是确保所有引用的使用都绝对安全,由编译器自动执行检查。但我无法抵挡诱惑,引入了空引用,仅仅是因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去四十年里,可能造成了十亿美元的痛苦和损失。”

rust 充分吸取了这个教训,彻底消除了空引用这个设计,换用 Option 枚举。跟上文的 Result<T, E> 类似, Option 枚举也需要穷尽匹配,强制要处理值为 None 时的情况。经过 rust 的锻炼,我相信程序员在转写 cpp 时,也能做到更有意识地考虑变量为空值时的情况(虽然这也本来就是应该的)。

没有空引用的意义远不止如此。在写 c 时,初学者甚至很多示范代码都会先用 NULL 去初始化一个指针,但 rust 中天然拒绝这种行为,且这种行为本身应该被杜绝。允许引用和指针为 NULL ,意味着当它悄咪咪的,不符合你预期的,真的变成 NULL 时,根本不会被发现。这也是前文十亿美元损失的根源。 rust 中,一旦引用为空编译器就会报错,这大大提高了代码的健壮性和最后成功的可能。这还保证了引用的生命周期一定短于原变量的生命周期(想象一下当你在原变量失效后,再使用它的引用会发生什么,就明白为什么这么说了)。

结尾

本来结尾不想起个标题的,但是感觉不起跟上一个部分的区分没那么明显。

从上面描述的特性不难看出, rust 的设计让它的编译非常强大。在 c 中可能因为空指针或者异常等导致运行崩溃的错误,在 rust 中在编译阶段就会被检查出来(因为这个原因 rust 被称为 agent 友好语言,当然这并没有定论),当然代价是程序员在写 rust 的时候也要比 cpp 考虑得更多,某种意义上把迟早要面对的复杂提前了。我个人认为这个提前是很有价值的,毕竟运行时的崩溃,可能真的会带来什么实际的损失,而写代码的时候考虑的多一点可能就是多掉些头发(。某种程度上。 rust 的特性也让程序员不能那么灵活的发挥,对于足够高手的程序员来说, cpp 可能更加自由,且能更加轻松的实现一些特殊的功能或逻辑;对于小白程序员来说, rust 也会显得过于复杂且难上手。但我认为,在从小白变成高手的早期,经受一下 rust 的拷打来磨练一下自己的思维还是很有意义的,就像前文说过的,你知道某个事然后选择去做不做,比不知道导致没得选要好太多。古人说的好,良药苦口利于病。

但事也并非绝对。 rust 中也有 unsafe rust 的存在,我对于这个领域目前还只是听过的阶段,不过相信大家听名字就能感觉到它比 safe rust 灵活。而 cpp 作为一门在高速迭代的语言,本身也在不断完善自己,甚至有人说现在的 cpp 和以前的 cpp 已经是两门语言了,只是还兼容以前 cpp 的写法而已。目前的 cpp 你可以调用各种库或者使用某些新的语法,来做到 rust 的大部分特性,而且它不强制你一定要使用。 cpp 这些新的特性没有 rust 这么出名的原因,我猜有两个:还没有 rust 做的那么好,毕竟设计哲学和底层还是不同;更新的太快,太难学了,哪怕程序员是一个终生学习的职业,依旧显得强度略高。尽管如此,我还是挺想学完 rust 之后去学学现代 cpp 的,顺便两相对照一下培养一下我的编程品味和审美。

最后的最后,由于本人 rust 并没有学完,所以本文并没有包含所有 rust 思维对写 cpp 有帮助的地方,比如竞争等。但我也不想等学完 rust 再来写这篇文章(不知道得到猴年马月了),且分开两次写还能水一水博客。后面如果有机会,估计会还有一篇博客谈论这个主题,所以或许有人可以注意到本篇博客的标题带了“初谈”二字,也算是我的一个美好期待,希望能有“再谈”的机会,希望那个时候不要太晚