Rust 是一种采用过去的知识解决将来的问题的技术。” ——Graydon Hoare
Rust 是一种快速、高并发、安全且具有授权性的编程语言,最初由 Graydon Hoare 于2006 年创造和发布。现在它是一种开源语言,主要由 Mozilla 团队和许多开源社区成员共同维护和开发。
虽然 Rust 是一种通用的多范式语言,但它的目标是 C 和 C 占主导地位的系统编程领域。这意味着你可以使用 Rust 编写操作系统、游戏引擎和许多性能关键型应用程序。同时,它还具有足够的表现力,你可以使用它构建高性能的 Web 应用程序、网络服务,类型安全的数据库对象关系映射(Object Relational Mapping,ORM)库,还可以将程序编译成WebAssembly 在 Web 浏览器上运行。Rust 还在为嵌入式平台构建安全性优先的实时应用程序方面获得了相当大的关注,例如 Arm 基于 Cortex-M 的微控制器,目前该领域主要由 C语言主导。Rust 因其广泛的适用性在多个领域都表现良好,这在单一编程语言中是非常罕见的。
Rust 作为一门静态和强类型语言而存在。静态属性意味着编译器在编译时具有所有相关变量和类型的信息,并且在编译时会进行大量检查,在运行时只保留少量的类型检查。它的强类型属性意味着不允许发生诸如类型之间自动转换的事情,并且指向整数的变量不能在代码中更改为指向字符串。例如在 JavaScript 等弱类型语言中,你可以轻松地执行类似“two = "2"; two = 2 two;”这样的操作。JavaScript 在运行时将 2 的类型弱化为字符串,因此会将 22 作为字符串存储到变量 two 中,这与你的意图完全相反并且毫无意义。在 Rust 中,与上述代码意义相同的代码是“let mut two = "2"; two = 2 two;”,该代码将会在编译时捕获异常,并提示信息:“cannot add '&str' to '{integer}'”。
因此,强类型属性使 Rust 可以安全地重构代码,并在编译时捕获大多数错误,而不是在运行时出错。用 Rust 编写的程序表现力和性能都非常好,因为使用它你可以拥有高级函数式语言的大部分特性,例如高阶函数和惰性迭代器,这些特性使你可以编译像 C/C 程序这样高效的程序。它的很多设计决策中强调的首要理念是编译期内存安全、零成本抽象和支持高并发。让我们来详细说明这些理念。
编译期内存安全:Rust 编译期可以在编译时跟踪程序中资源的变量,并在没有垃圾收集器(Garbage Collectors,GC)的情况下完成所有这些操作。
这意味你不会遇到在 free、double free 命令之后调用指针,或者运行时挂起指针等“臭名昭著”的问题。Rust 中的引用类型(类型名称前面带有&标记的类型)与生命周期标记隐式关联('foo),有时由程序员显式声明。在生命周期中,编译器可以跟踪代码中可以安全使用的位置,如果它是非法的,那么会在编译期报告异常。为了实现这一点,Rust 通过这些引用上的生命周期标签来运行借用/引用检查算法,以确保你永远不能访问已释放的内存地址。这样做也可以防止你释放被其他某些变量调用的任何指针。
零成本抽象:编程的目的就是管理复杂性,这是通过良好的抽象来实现的。接下来让我们来看一个 Rust 和 Kotlin 的良好抽象示例。抽象让我们能够编写高级并且易于阅读和推断的代码。我们将比较 Kotlin 的流和 Rust 的迭代器在处理数字列表时的性能,并参照 Rust提供的零成本抽象原则。这里的抽象是指能够使用以其他方法作为参数的方法,根据条件过滤数字而不使用手动循环。在这里引入 Kotlin 是因为它看上去和 Rust 存在相似性。代码很容易理解,我们的目标是给出更高层面的解释,并对代码中的细节进行详细阐述,因为这个示例的重点是理解零成本特性。
首先,我们来看 Kotlin 中的代码:
1. import Java.util.stream.Collectors
2.
3. fun main(args: Array<String>)
4. {
5. //创建数字流
6. val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).stream()
7. val evens = numbers.Filter { it -> it % 2 == 0 }
8. val evenSquares = evens.map { it -> it * it }
9. val result = evenSquares.collect(Collectors.toList())
10. println(result) // prints [4,16,36,64,100]
11.
12. println(evens)
13. println(evenSquares)
14. }
我们创建了一个数字流(第 6 行)并调用了一系列方法(filter 和 map)来转换元素, 以收集仅包含偶数的序列。这些方法可以采用闭包或函数(第 8 行中的“ it -> it * it”)来转换集合中的元素。在函数式编程语言中,当我们在流/迭代器上调用这些方法时,对于每个这样的调用,该语言会创建一个中间对象来保存与正在执行的操作有关的任何状态或元数据。因此,evens 和 evenSquares 将在 JVM 堆上分配两个不同的中间对象。在堆上分配资源将会产生内存开销,这是我们在 Kotlin 中为抽象必须额外付出的代价。
当我们输出 evens 和 evenSquares 的值时,确实得到了两个不同的对象,如下所示:
java.util.stream.ReferencePipeline$Head@51521cc1
java.util.stream.ReferencePipeline$3@1b4fb997
@之后的十六进制值是 JVM 对象的哈希值。由于哈希值不同,所以它们是不同的对象。在 Rust 中,我们会做相同的事情:
1. fn main() {
2. let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_iter();
3. let evens = numbers.filter(|x| *x % 2 == 0);
4. let even_squares = evens.clone().map(|x| x * x);
5. let result = even_squares.clone().collect::<Vec<_>>();
6. println!("{:?}", result); // 输出 [4,16,36,64,100]
7. println!("{:?}\n{:?}", evens, even_squares);
8. }
接下来将解释上述代码的细节。在第 2 行中,我们调用 vec![]创建一个数字列表,然 后调用 into_iter()方法使其成为一个数字的迭代器/流。使用 into_iter()方法从集合中创建 了一个包装器的迭代器类型(这里 Vec<i32>是一个有符号的 32 位整数列表),即 IntoIter([1,2,3,4,5,6, 7,8,9,10]),此迭代器类型引用原始的数字列表。然后我们执行 filter 和 map 转换(第 3 行和第 4 行),就像我们在 Kotlin 中所做的那样。第 7 行输出 evens 和 even_squares 的类型,如下所示(为了简洁,省略了一些细节):
evens: Filter { iter: IntoIter( <numbers> ) }
even_squares: Map { iter: Filter { iter: IntoIter( <numbers> ) }}
中间对象 Filter 和 Map 是基础迭代器结构上的包装器类型(未在堆上分配),它本身是一个包装器,包含对第 2 行的原始数字列表的引用。第 4 行和第 5 行的包装器结构在分别调用 filter 和 map 时创建,它们之间没有任何指针解引用,并且不会像 Kotlin 那样产生堆分配的开销。所有这些可归结为高效的汇编代码,这相当于使用循环(语句)的手动编写版本。
支持高并发:当我们说 Rust 是并发安全的时,其含义是该语言具有应用程序接口(Application Programming Interface,API)和抽象能力,使得编写正确和安全的并发代码变得非常容易。而在 C 中,并发代码出错的可能性非常大。在 C 中同步访问多个线程的数据时,需要在每次进入临界区时调用 mutex.lock(),并在退出它时调用 mutex.unlock():
// C
mutex.lock(); // 互斥锁锁定
// 执行某些关键操作
mutex.unlock(); // 执行完毕
在大量开发人员共同协作的大型代码库中,你可能会忘记在多线程访问共享对象之前调用 mutex.lock(),这可能导致数据访问冲突。在其他情况下,你可能忘记解开互斥锁(Mutex),并使其他想要访问数据的线程一直处于等待状态。
Rust 对此有不同的处理方式。在这里,你将数据包装成 Mutex 类型,以确保来自多个线程的数据进行同步可变访问:
// Rust
use std::sync::Mutex;
fn main() {
let value = Mutex::new(23);
*value.lock().unwrap() = 1; // 执行一些修改
} // 这里自动解锁
在上述代码中,我们能够在变量 value 调用 lock()方法之后修改数据。Rust 采用了保护共享数据自身,而不是代码的概念。Rust 与 Mutex 和受保护的数据的交互并不是独立的,这和 C 中的情况一样。你无法在 Mutex 类型不调用 lock()方法的情况下访问内部数据。
那么 lock()方法的作用是什么?调用 lock()方法之后会返回一个名为 MutexGuard 的东西,它会在变量超出作用域范围之后自动解除锁定,它是 Rust 提供的众多安全并发抽象之一。
另一个新颖的想法是标记特征的概念,它在编译期验证,并确保在并发代码中同步和安全地访问数据,第 4 章详细介绍了该特征。类型会被称为 Send 和 Sync 的标记特征进行注释标记,以指示它们是否可以安全地发送到线程或者在线程之间共享。当程序向线程发送值时,编译器会检查该值是否实现了所需的标记特征,如果没有,则禁止使用该值。通过这种方式,Rust 允许你毫无顾虑地编写并发代码,编译器在编译时会捕获多线程代码中的异常。
编写并发代码已经很难了,使用 C/C 会让它变得更加困难和神秘。当前 CPU 没有获得更多的时钟频率;相反,我们添加了更多内核。因此,并发编程是正确的发展方向。Rust 使得编写并发代码变得轻而易举,并且降低了编写安全的并发代码的门槛。
Rust 还借鉴了 C 的 RAII 原则用于资源初始化,这种技术的本质是将资源的生命周期和对象的生命周期绑定,而堆分配类型的解除分配是通过执行 drop 特征上的 drop()方法实现的。当变量超出作用域时,程序会自动调用此方法。它还用 Result 和 Option 类型替代了空指针的概念,我们将在第 6 章对此进行详细介绍。这意味着 Rust 不允许代码中出现null/undefined 的值,除非通过外部函数接口与其他语言交互,以及使用不安全代码时。该语言还强调组合,而不是继承,并且有一套特征系统,它由数据类型实现,类似于 Haskell的类型类,也被称为加强型的 Java 接口。
但同样重要的是,Rust 社区非常活跃和友好。该语言包含非常全面的文档,可以在Rust 官网中找到。Rust 在 Stack Overflow 的开发者调查上连续 3 年(2016 年、2017 年和2018 年)被评为最受欢迎的编程语言,因此编程社区对它非常青睐。总而言之,如果你希望编写具有较少错误的高性能软件,又希望感受当前流行语言的特性和极佳的社区文化, 那么 Rust 应该是一个不错的选择。
推荐阅读精通Rust 第2版
Rust是一门系统编程语言,是支持函数式、命令式以及泛型等编程范式的多范式语言。Rust在语法上和C 类似。Rust快速、可靠、安全,它提供了甚至超过C/C 的性能和安全保证,同时它也是一种学习曲线比较平滑的热门编程语言。
本书内容共17章,由浅入深地讲解Rust相关的知识,涉及基础语法、软件包管理器、测试工具、类型系统、内存管理、异常处理、高级类型、并发模型、宏、外部函数接口、网络编程、HTTP、数据库、WebAssembly、GTK 框架和GDB调试等重要知识点。
本书适合想学习Rust编程的读者阅读,希望读者能够对C、C 或者Python有一些了解。书中丰富的代码示例和详细的讲解能够帮助读者快速上手,高效率掌握Rust编程。