Rust 是一门比较新的系统编程语言。Rust 在语法上和 C++ 与 Haskell 比较类似,支持函数式和命令式以及泛型等多种编程范式,在保证性能的同时提供更好的内存安全策略。Rust 最初由 Mozilia 研究院的 Graydon Hoare 设计创造,然后在 Dave Herman, Brendan Eich 以及很多人的贡献下逐步完善。第一个有版本号的 Rust 编译器于2012 年1月发布。Mozilla 在2014年10月宣布发布 Rust 编译器和工具的0.12版。Rust 1.0 是第一个稳定版本,于 2015年5月15日发布。当前(2022年7月8日)的最新版本是 1.62.0。

Rust 工具箱

不同于其他编程语言需要由开发者来共建生态,Rust 社区提供了一揽子的生态工具和 Rust 语言配合,这个工具相当于是官方支持的,所以被广泛接受。

工具 作用 类比
Rustup Rust 的安装器和版本管理工具。可以通过 rustup update 来更新 Rust 的版本。 nvm
Cargo Rust 的构建工具和包管理器。可以通过 cargo --version 来查看 Rust 和 Cargo 的版本,他们的版本始终是保持一致的。 npm/yarn
crates.io Rust 包的仓库 npmjs.com
Rustfmt Rust 的代码风格格式化工具。 prettier
Clippy Rust 的代码风格检查工具。 eslint/standard
Rust Language Server(LSP) 为 Rust 提供编辑器和IDE支持
Rustdoc Rust 的文档生成器
Rust 官方 Discord Rust 相关问题的讨论区
Rust stackoverflow tag Rust 相关问题提问区
Rust 官方用户论坛 Rust 用户论坛 -
TOML Cargo 配置文件格式

开始 Hello World!

首先,需要安装 Rust 及相关生态工具,以下均以类 Linux 系统为例:

1
2
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # 安装 Rustup & Cargo
cargo --version # 查看 Rust 版本,验证是否正确安装

创建一个新的 Rust 项目

1
2
3
4
5
6
7
8
9
10
cargo new hello-rust # 生成一个新的“Hello, world!”项目

# 包含以下文件
# hello-rust
# |- Cargo.toml # Rust 的清单文件。其中包含了项目的元数据和依赖库。
# |- src
# |- main.rs # 编写应用代码的地方。

cd hello-rust
cargo run # 执行程序

这样就可以在控制台看到 Hello, world! 的提示语。
在 Cargo.toml 文件中添加以下信息

Cargo.toml
1
2
[dependencies]
ferris-says = "0.2"

在 main.rs 中添加以下代码

main.rs
1
2
3
4
5
6
7
8
9
10
11
use ferris_says::say;
use std::io::{stdout, BufWriter};

fn main() {
let stdout = stdout();
let message = String::from("Hello fellow Rustaceans!");
let width = message.chars().count();

let mut writer = BufWriter::new(stdout.lock());
say(message.as_bytes(), width, &mut writer).unwrap();
}

执行命令

1
2
3
4
5
6
7
8
9
10
11
12
cargo build # 安装依赖。此命令会创建一个新文件 Cargo.lock,该文件记录了本地所用依赖库的精确版本。
cargo run # 执行程序
# 执行结果:
# __________________________
# < Hello fellow Rustaceans! >
# --------------------------
# \
# \
# _~^~^~_
# \) / o o \ (/
# '_ - _'
# / '-----' \

语言特性

Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
main 函数是 Rust 程序的入口。
Rust 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。
Rust 是一门基于表达式(expression-based)的语言。
Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 if 的条件。

关键字

运算符与符号

可派生的trait

语法

关键字与标志符 用法 作用 类比
use use std::io; 显示的引入 import
fn fn main(){} 函数声明 function
let let guess = 5; 变量声明,在 Rust 中,变量默认是不可变的 const
const const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; 常量声明 const
mut let mut guess = 5 可变变量声明 var
:: let mut guess = String::new(); 关联函数(associated function), 也就是类的静态方法(static method) .
& io::stdin().read_line(&mut guess) 引用(reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。
match match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } 一个 match 表达式由 分支(arms) 构成。一个分支包含一个 模式(pattern)和表达式开头的值与分支模式相匹配时应该执行的代码。Rust 获取提供给 match 的值并挨个检查每个分支的模式。 switch (${cmp}) { case ${condition}: ${action}}
loop loop {} 循环 while(true) {}

概念

预导入 prelude

默认情况下,Rust 设定了若干个会自动导入到每个程序作用域中的标准库内容,这组内容被称为 预导入 (prelude)内容。

隐藏 shadow

Rust 允许用一个新值来 隐藏 (shadow)变量之前的值。这个功能常用在需要转换值类型之类的场景。

数据类型 data type

在 Rust 中,每一个值都属于某一个 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。

Rust 有两种数据类型子集 标量(scalar) 和 复合(compound)。

标量类型

Rust 有四种基本的标量类型:整数(integers)、浮点数(floating-point numbers)、布尔类型(booleans)和字符类型(characters)。

整数包括:

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

Rust 也有两个原生的浮点数类型 f32 与 f64。

Rust 中的布尔类型有两个可能的值:true 和 false。Rust 中的布尔类型使用 bool 表示。

Rust的 char 类型是语言中最原生的字母类型。单引号声明字面量,双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value)。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。

复合类型

Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。

1
2
3
4
let tup: (i32, f64, u8) = (500, 6.4, 1); // 元组的声明
println!(tup.0) // 打印 500。 使用 . 操作符来访问元组的值
let (x, y, z) = tup; // 使用模式匹配(pattern matching)来解构(destructure)元组值
println!(y) // 打印 6.4

Rust 中的数组与一些其他语言中的数组不同,Rust中的数组长度是固定的。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。

1
2
3
let a: [i32; 5] = [1, 2, 3, 4, 5]; // 数组声明
let b: [3 ; 5]; // 每个元素都为相同值的数组声明
let first = a[0]; // 使用索引来访问数组的元素

循环标签 loop label

如果存在嵌套循环,break 和 continue 应用于此时最内层的循环。你可以选择在一个循环上指定一个 循环标签(loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环。循环标签以 单引号 开始, 冒号 结束,例如 'counting_up:

所有权 ownership

  1. Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

针对于内存与分配,Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop。Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用,否则,赋值操作会使旧的变量无效,这个操作被称为 移动(move)。如果我们 确实 需要深度复制未实现 Copy trait 的类型,可以使用一个叫做 clone 的通用函数。

以下类型实现了 Copy trait:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会 移动 或者 复制 ,就像赋值语句一样。返回值也可以转移所有权。

引用与借用 references & borrowing

在 Rust 中, & 符号就是 引用,它们允许你使用值但不获取其所有权。与使用 & 引用相反的操作是 解引用(dereferencing),它使用解引用运算符 *。我们将创建一个引用的行为称为 借用(borrowing),(默认)不允许修改引用的值。

&mut 为可变引用(mutable reference),可变引用允许我们修改一个借用的值,但在同一时间只能有一个对某一特定数据的可变引用。

作用域 scope

作用域是一个项(item)在程序中有效的范围。变量作用域就是变量在程序中有效的范围。

一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。

非词法作用域生命周期 NLL

编译器在作用域结束之前判断不再使用的引用的能力被称为 非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)

泛型 generics

泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。

单态化 monomorphization

单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。我们可以使用泛型来编写不重复的代码,而 Rust 将会为每一个实例编译其特定类型的代码。

特质 trait

一种定义泛型行为的方法。特质可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。

生命周期 lifetimes

生命周期是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。

相干性 coherence(孤儿规则 orphan rule)

只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。这个限制是被称为 相干性(coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。

问题

不可变变量和常量有什么区别?

  • 不允许对常量使用 mut。
  • 常量不光默认不能变,它总是不能变。
  • 声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型。
  • 常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。
  • 常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值。

隐藏(shadow)和将变量标记为 mut 有什么区别?

  • 当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不可变的。
  • 当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字。

整型溢出会发生什么?

有一个 u8 ,它可以存放从 0 到 255 的值。当你将其修改为 256 时就会发生 “整型溢出”(“integer overflow”)。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic(程序因错误而退出)。在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。简而言之,值 256 变成 0,值 257 变成 1,依此类推。

数组下标越界会发生生么?

当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic。

语句与表达式有什么区别?

语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。

为什么在同一时间内对某一特定数据不能存在多个可变引用?

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:

两个或更多指针同时访问同一数据。
至少有一个指针被用来写入数据。
没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生。

Rust是否会存在悬垂引用(Dangling References)的问题?

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

Rust的模块系统(the module system)包含哪些部分?

  • 包(Packages): Cargo 的一个功能,它允许你构建、测试和分享箱(crate)。
  • 箱(Crates):一个模块的树形结构,它形成了库或二进制项目。
  • 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径(path):一个命名例如结构体、函数或模块等项的方式。

为什么 Rust 不允许使用索引获取 String 字符?

  1. String 是一个 Vec<u8> 的封装,每个 Unicode 标量值需要两个字节存储,因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值
  2. 索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符

如何决定何时应该使用 panic! 以及何时应该返回 Result 呢?

panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。

写出三种方法给定 vector 列表返回出其中的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
fn largest<T: PartialOrd + Clone>(list: &[T]) -> T {
let mut largest = list[0].clone();
for item in list {
if item > &largest {
largest = item.clone();
}
}
largest
}

fn largest1<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}

fn largest2<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if &item > &largest {
largest = &item;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest2(&number_list);
println!("The largest number is {}", result);

let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest2(&char_list);
println!("The largest char is {}", result);
}