Appearance
本篇开始将为各位带来 Rust 这门后端语言。这门语言重点强调内存安全、高性能。在很多时候,内存安全和性能往往是不可兼得的存在:
- 如果追求高性能,像 C/C++,让程序员手动管理内存的分配和释放,那么很显然并不安全,很容易因为程序员而导致内存泄露和错误频发。
- 如果追求安全性,像 Java/Python,引入垃圾回收系统(GC),垃圾回收会带来运行时的暂停(GC Pause),这会带来性能损失和额外的内存开销。
那么 Rust 是怎么做到即兼顾了性能又能做到极高的安全性?首先它不使用垃圾回收,从源头上解决了运行时不必要的性能问题和内存开销,其次,就是依赖于 Rust 三大核心特性。
我们来简单了解一下,在后面的课程中,我们会重点学习。
1. 所有权系统 Ownership
Rust 没有垃圾回收,也不能让程序员自己去分配和释放内存(这会导致安全问题),那么我们就必须想一个解决方法,能够让程序员写出安全的代码。
该如何解决呢?Rust 想到了一个非常好的解决方案,既然垃圾回收的停顿是运行时存在的,那么我是不是可以把它放到编译时?这是一个堪称天才的想法。
Rust 将内存该怎么分配,什么时候释放给放到了编译时,编译时期就能决定的东西,就不会再运行时出现错误。
当然,这非常依赖于 Rust 的编译器,现在的编译器可能还不够完美,但是在未来,Rust 还会引入更多优秀的特性。
例如在 C/C++ 中,内存是一块公共的内容,程序员可以随意访问和清理内存。这就会因为程序员在开发时的不合理声明、清除内存,导致程序崩溃、内存泄漏甚至更严重的问题。
而 Rust 则像是一场接力比赛,每一个数据(接力棒)在一个时间里都只存在一个主人(运动员),如果你把这个接力棒给了下一个运动员,这个叫做 “移动”(Move),上一个运动员就不再拥有这个接力棒了,这个接力棒的所有权被移动到了现在这个运动员手中。
这个比喻就展现了 Rust 的所有权系统 —— 东西只能有一个主人。
2. 借用 Borrowing
我们再举一个例子:图书馆。
小李有一本书:《Rust语言圣经》,小李寻思着 “自己都看完了,要不借给图书馆给别人看吧”,随后小李把图书借给了图书馆。然后小明到了图书馆,看到了这本书,于是借走了这本书,这叫做借用(Borrowing)。
那么这就产生了一个问题,这本书的所有权是谁的?小李?小明?还是图书馆的?
答案是小李的,小李拥有书的所有权,只不过是把书借给了图书馆,然后图书馆再借给了小明。小李对书的所有权在借用的过程中不会发生移动(Move)。
而借用有哪些特性呢?
不可变借用(只读)
图书馆可以把书借给任何人看,所有人都可以借用这本书,但是大家不能在图书上乱涂乱画。
可变借用(独占修改)
如果图书有错,有人想要对这本书进行校对纠错,那么图书馆就会对这本书采取封闭的措施,只有这一个人可以看和修改,其他人,连看都不准看。
在 Rust 官方教材《The Rust Programming Language》中,有这样一段总结:
At any given time, you can have either one mutable reference or any number of immutable references.
在任意时刻,你要么只能有一个可变引用,要么可以有任意数量的不可变引用。
TIP
在 Rust 中,“借用” 和 “引用” 被认为是同义词,引用(Reference)是具体的那个东西,借用(Borrowing)则是行为。不过它们在描述同一个特性。
3. 零成本抽象 Zero-cost Abstractions
这是 Rust 核心特性中的最后一个重要特性 —— 零抽象成本。它是由 C++ 之父 Bjarne Stroustrup 提出的,总结起来是两句话:
- 你没用到的东西,你不必为之付出代价(没有多余的运行时开销)。
- 你用到的东西,你自己手写代码也不可能做得更好(编译器生成的汇编代码已经达到了最优状态)。
什么是抽象?
抽象(Abstract)并不是艺术作品中的抽象画作,我们可以用一句话概括它:隐藏具体的实现细节,只暴露功能的接口。
我们从三个方面讨论 Rust 的零抽象成本:
1. 从如何做到做什么
抽象可以让你用声明式(Declarative)的代码来告诉计算机做什么,而非通过命令式(Imperative)的代码告诉计算机怎么做。
声明式就像去饭店里点单,坐在位子上然后对服务员说要一份西红柿炒鸡蛋。命令式则像是你在教一个人怎么做炒鸡蛋,他需要到厨房,打开冰箱拿出鸡蛋,拿口锅,开火倒油炒鸡蛋然后怎么怎么的。
在声明式的情况下,你只表达了你的目标,也就是最终的结果,而具体的执行步骤(拿蛋、开火、翻炒)被隐藏在了 “炒鸡蛋” 这个抽象概念之后。
2. 建立通用模型(Trait)
抽象意味着你需要定义一份通用的模型,还是炒鸡蛋这个例子,虽然它很简单,我们认为炒鸡蛋该怎么炒可以被归入常识,那么在这里的常识就是一份通用模型。拿锅、开火、倒油、炒鸡蛋,最后可能再调点味。
这份通用模型可以被任何人运用,它有一个最基础的得出结果(没有调味的炒鸡蛋),而后续的行为(调味),就不是一个通用的模型了,你可以根据自己的口味和喜好去调自己喜欢的味道。
3. 心理模型的简化
注意
Rust 的零抽象成本的 “零” 不是抽象,反之,抽象其实越多越好;零的实际是是成本,这句 “零抽象成本” 我们可以理解为 “零成本的抽象”。
抽象是一种对程序员的减负,它把复杂的硬件操作封装成人类易于理解的概念。
| 抽象概念 | 它背后隐藏的细节 |
|---|---|
| 所有权 (Ownership) | 内存的申请、释放、悬垂指针检查、双重释放错误。 |
| 闭包 (Closures) | 环境变量的捕获、堆栈分配、函数指针的转换。 |
| Async/Await | 状态机的切换、事件轮询、上下文保存、非阻塞 I/O。 |
如果 Rust 没有为我们实现所有权、闭包、异步,那么上述的这些细节就需要留给 Rust 程序员们,这是非常痛苦的部分。而 Rust 的零抽象成本,为你把这些细节抹去,它们提供了最优解,并且为你提供了通用实现,你只需要理解函数的结果,而非实现过程,这就是零抽象成本。