The Rust Programming
Language 中译本
配套材料:Rust By Example
中译本
第零~二章 Rust 入门
- 个人收集的几个国内比较流行的中文社区:
- rust-lang-cn:Rust
中文官网提到的“非官方翻译”由此组织提供,维护了目前最全的 中文 Wiki;
- rustcc:维护了中文社区的
https://github.com/rustcc/awesome-rust,翻译了 Rust Atomics and
Locks、Rust Primer、Write an OS in Rust
等书籍,保存了一些国内大会讲义;
- rustlang-cn:维护了非常完善的
锈书 https://github.com/rustlang-cn/rusty-book,以及
Rustt、rust-weekly、rust-algos、Asynchronous Programming in
Rust 翻译 等项目;
- sunface:有 Rust 语言圣经 和 rust-by-practice;
- 安装(MacOS):VSCode 扩展:rust-analyzer,Sublime
Text 扩展:https://github.com/rust-lang/rust-enhanced
1 2 3 4 5 6 7
| $ xcode-select --install $ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh Rust is installed now. Great! $ rustup update $ rustup self uninstall $ rustc --version $ rustup doc
|
- 可以使用
rustfmt
格式化源码;标准写法大括号不换行(有空格);
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
| use rand::Rng; use std::cmp::Ordering; use std::io;
fn main() { println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
loop { println!("Please input your guess.");
let mut guess = String::new();
io::stdin() .read_line(&mut guess) .expect("Failed to read line");
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, };
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
|
- Rust 是静态强类型语言,
rustc main.rs
得到可执行文件 main
;
- Cargo:
cargo new hello_cargo
创建目录并自动生成 TOML
文件;
cargo build
构建,cargo run
构建并运行,cargo check
确保能编译但不生成可执行文件;
cargo build --release
发布;cargo doc --open
用网页打开项目依赖库的手册;
- 在 Cargo.toml 中使用
[dependencies]
添加依赖
rand = "0.8.3"
,此时会寻找包含 0.8.3 但低于 0.9.0
的版本,若没有则从 Crates.io
下载;Cargo.lock 会记录所有可用版本,此后出发更新 toml 文件,否则 Cargo
不会主动更新版本;
第三章 通用编程概念
- 可变性:
let
变量默认不可变(immutable),可以用 mut
使其可变;
- 可以再次使用
let
遮蔽(shadow)前一个变量(两个变量完全独立,也不需要是同一类型),作用域的遮蔽与其它语言类似:更深一层作用域中的遮蔽在作用域结束后失效;
const
声明编译期常量,变量名规范为
SCREAMING_SNAKE_CASE;
- 标量类型:
- 整型:i8/16/32(默认)/64/128/size,u8/16/32(默认)/64/128/size;
isize
和 usize
视 32/64 位系统而具有 32/64
位;字面量允许添加无效的 _
便于阅读;
- 浮点型:f32/64(默认),使用
IEEE-754;布尔类型:bool;字符类型:char(Unicode),字面量使用单引号;
- 复合类型:
- 元组:使用解构或索引形式访问元组元素,无值元组
()
称为单元类型,该值称为单元值,是不返回任何其它值的表达式的隐式返回值;
- 数组:
let a: [i32; 5] = [1, 2, 3, 4, 5];
,包含 5 个 3
的数组: let a = [3; 5]
;超出索引范围时会
panic;
- 函数:函数和变量命名均使用小写蛇形;函数定义相互顺序没有影响;函数签名必须声明每个参数的类型;可以用尾置返回值;
- Rust 是一门基于表达式的语言:
- 语句(statement):执行一些操作但不返回值;Rust
中没有连等,因为
let
是语句,无法再参与赋值;
- 表达式(expression):计算并产生一个值;函数调用、宏调用、创建新作用域的大括号(代码块)(最后一句无分号)均是表达式;
- 注释:双斜杠注释,一般置于需要解释的代码行上一行;块注释
/*...*/
;
- 控制流:
if
语句条件不需要括号,条件必须是一个 bool
,每个分支(arm)为一对大括号括起的语句或表达式(此时可以用于赋值);短路求值;
loop
语句支持使用循环标签 'lable
以供
continue
和 break
使用, break
还可以后跟一个值作为表达式的返回值;
- 另外两种循环:
while condition {...}
,
for element in a
;
第四章 认识所有权
- Rust
中每个值有且仅有一个所有者(Owner)变量,所有者离开作用域时值被丢弃(调用特殊函数
drop
,类似于 C++ 中的 RAII);
- 以 String 为例:可变、可增长字符串(存于堆中);在
let s2 = s1;
赋值时使用移动语义, s1
不再可用;Rust 不会自动执行深拷贝,需要深拷贝时应使用 clone
方法;
- 实现了
Copy
trait
的类型在赋值给其它变量后仍可使用,实现了 Drop
trait
的类型不可使用 Copy
trait;实现了 Copy
trait
的类型:所有标量类型与仅包含标量类型的元组;
- 函数参数传递与赋值相似,会转移所有权,非引用形参离开函数作用域后(若没有被返回)会被
drop
,原函数中不再可用;
- 创建引用的行为称为借用,借用的变量不可修改;可变引用不可与同一变量的任何其它引用的作用域重叠;注意引用的作用域从声明处到最后一次使用为止;
- 编译器不允许悬垂引用(Dangling References);
- 切片(Slice):
let hello = &s[0..5];
;字符串字面量let s = "Hello, world!";
中 s
的类型是 &str
,是指向二进制程序特此位置的
slice,因此为不可变引用;
第五章
使用结构体组织关联数据
- 结构体(struct):若
user1
中有未实现
Copy
trait 的成员,则不能再使用 user1
;元组结构体:
struct Color(i32, i32, i32);
;
1 2 3 4
| let user2 = User { email: String::from("another@example.com"), ..user1 };
|
- 方法:
- 在
impl
块中定义,使用 self
访问结构体成员;
- getters 允许我们将字段变为私有而方法是公共的;
- Rust 会自动为
object
添加 &
、
&mut
或 *
以与方法签名匹配;
- 在
impl
块中定义但不以 self
为第一参数的函数称为关联函数或静态方法,常被用作构造函数;
&self
是 self: &Self
的语法糖;
- 自动引用和解引用(automatic referencing and
dereferencing):使用
obj.something()
调用方法时,Rust
会自动为 obj
添加 &
、&mut
或 *
以便使 obj
与方法签名匹配,可以自动添加任意多层;
第六章 枚举和模式匹配
- 枚举提供如下操作;Option<T>:用于处理空值的枚举,由编译器确保我们处理了空值的情况;
1 2 3 4 5 6 7 8 9 10 11 12
| enum IpAddr { V4(u8, u8, u8, u8), V6(String), }
let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));
enum Option<T> { Some(T), None, }
|
- match 控制流运算符:
- 支持枚举类型,
=>
后跟表达式;
- match 可以与
Option<T>
匹配,分支模式若为枚举可以带参数;
- 提供其它分支模式
other
,模式按顺序依次匹配,因此
other
应置于最后;
- 通配符
_
可以匹配任意值而不绑定;
- if let 是
match
的语法糖,当值匹配某一模式时执行代码而忽略其它值;
1 2 3 4 5 6 7 8 9 10 11 12 13
| let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, }
if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; }
|
第七章
使用包、Crate 和模块管理不断增长的项目
- Crate:一个二进制项或者库;包(Package):至多包含一个库
crate,可以包含任意多个二进制 crate;
- 模块系统:
- 使用
cargo new --lib lib_name
新建库;
- 使用
mod
定义当前文件模块内容或引入同名文件/文件夹所定义的模块;
- 使用
pub
公开函数和成员,使用 use
引入模块路径,使用 as
给引入模块起别名;
- 使用
pub use
重导出;
- 使用嵌套路径:
use std::io::{self, Write};
,通过 glob
运算符 *
引入所有公有定义(不推荐);
第八章 常见集合
- vector:
- 有些类似 C++; 使用
push()
和 pop()
,
reserve()
预留内存;
Vec<T>
或使用 vec!
宏;
[]
下标超出范围会 panic,但 get()
会返回
None
;
- 可以使用枚举使
vector
存储不同的值;
String
及其 slice 均为 UTF-8 编码,
let s3 = s1 + &s2;
会让 s1
失去所有权;不支持索引,因为 String
是
Vec<u8>
的封装,遍历时需要使用 chars()
(不能获得字形簇);
- HashMap:
1 2 3 4 5 6 7 8 9
| use std::collections::HashMap;
let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
let count = scores.entry(String::from("Yellow")).or_insert(50); *count += 1;
|
第九章 错误处理
panic!
宏使程序打印错误信息,展开并清理栈数据,然后退出;
unwrap
和 expect
方便地提供错误信息,在
Result 返回 Err 时直接 panic;
1 2 3 4
| enum Result<T, E> { Ok(T), Err(E), }
|
- 通过传播(propagating)将错误传递给函数调用者;
- 使用
?
运算符调用 from
函数将收到的错误类型转换为由当前函数返回类型所指定的错误类型,并返回给上层调用者;
1 2 3 4 5
| impl From<CreationError> for ParsePosNonzeroError { fn from(err:CreationError) -> ParsePosNonzeroError { ParsePosNonzeroError::Creation(err) } }
|
第十章 泛型、trait 与生命周期
- 使用
<T>
定义函数或结构体泛型,使用
impl<T> Point<T>
定义泛型方法,使用
impl Point<f32>
定义具体类型方法;
- trait:
- 类似其它语言的接口,定义内部有多个函数签名;
- 使用
impl MyTrait for MyStruct
为结构体实现
trait,trait 可以定义默认实现;
- 只能实现本地作用域下的类型而不能实现引入的外部类型;
- 使用
item: impl MyTrait
传参实现了此 trait 的类型,通过
+
要求实现多个 trait;
- trait bound:trait 是 trait bound
的语法糖,后者可以强制函数传递的两个实现此 trait 的参数是同一类型;
- 借用检查器(borrow
checker):被引用者的生命周期必须包含它的引用变量的生命周期;
- 泛型生命周期:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str;
, 'a
会对 x
和 y
的生命周期取交集,生命周期标注不改变任何生命周期;使用生命周期标注定义包含引用的结构体;
- 生命周期省略规则:
- 每个引用参数都有自己的生命周期;
- 如果只有一个输入生命周期参数,就将其赋予所有输出生命周期参数;
- 如果方法参数中有
&self
或
&mut self
,说明是对象方法,此时所有输出生命周期参数被赋予 self
的生命周期;
'static
拥有整个程序的生命周期;实现函数时可以加
mut
或不加,函数声明时不能添加pattern
- 阶乘的函数式写法:
(1..=num).product()
;
第十一章 编写自动化测试
- 编写测试:
- 使用
#[cfg(test)]
和 #[derive(Debug)]
开启调试能力;
- 使用
#[test]
与 cargo test
测试该函数是否
panic;
- 使用
assert!
宏使程序在条件为假时 panic,使用
assert_eq!
和 assert_ne!
测试相等(还需要
derive(PartialEq)
),三种 assert
均支持附带一个显示出错信息参数;
Debug
trait 提供使用 {:?}
打印类型调试格式,使用 {:#?}
打印更美观的调试格式;
- 使用
#[test]
后跟 #[should_panic]
使 panic
时通过测试,使用 #[should_panic(expected = "...")]
限定接收到的 panic 信息;
- 运行测试:
- 默认为并行测试,使用
cargo test -- --test-threads=1
使用串行;
- 只有测试失败的函数会显示正常输出内容,测试通过的函数不会再显示其输出,使用
cargo test -- --nocapture
显示通过测试的值;
- 使用
cargo test name
测试所有函数名中包含 name
的函数;
- 使用
#[test]
后跟 #[ignore]
忽略此测试函数;
- 集成测试:
- 在项目根目录创建 tests 目录,不再需要
#[cfg(test)]
;
- 使用
cargo test --test name
指定测试 name
文件中的测试函数;
- 将提供测试专用的公开函数的文件置于
tests/common/mod.rs;
第十二章 一个 I/O
项目:构建命令行程序
- 二进制项目关注分离:将具体功能提取到 lib.rs
中,main.rs 仅处理程序运行;
- 使用
Result
而不是直接 panic,从 main
中提取 run
的逻辑,并捕获其返回的错误;
- 测试驱动开发(Test Driven Development,
TDD):编写期望失败的测试,再编写代码使其通过,然后重构增加的代码;
- 使用
eprintln!
打印信息到标准错误流;
fmt::Debug
:使用 {:?}
或
{:#?}
标记,格式化文本以供调试使用,可以直接
derive;fmt::Display
:使用 {}
标记,以更优雅和友好的风格来格式化文本,需要手动实现;
第十三章
Rust 中的函数式语言功能:迭代器与闭包
闭包(closures):
- 可以保存进变量或作为参数传递给其他函数的匿名函数,可以捕获调用者作用域中的值。
- 闭包不暴露给用户,因此可以没有参数和返回值类型标注,当然也可以标注:
|num: u32| -> u32
;对于没有类型标注的闭包,在第一次调用后编译器会自动绑定上所有类型,后续不能再改变;
- 每个闭包实例有其自己独有的匿名类型;下面是一个值在第一次调用时确定的函数闭包实现:
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
| struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, }
impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } }
fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } }
|
- 闭包会捕获其环境,实现了 trait
Fn
(获取不可变借用值)、 FnMut
(获取可变借用值)或
FnOnce
(获取环境所有权)中的至少一个;
迭代器:
Iterator
trait 实现了
fn next(&mut self) -> OptionSelf::Item;
;
iter
产生不可变引用, into_iter
产生拥有所有权的迭代器, iter_mut
返回可变引用;
- 迭代器会提供一些消费迭代器自身的方法(称为消费适配器,如
v1.iter().sum()
、 collect()
),它们会调用
next
;
- 迭代器适配器:
v1.iter().map(|x| x + 1);
、
v1.into_iter().filter(|x| x > 1);
;
- 由于 Rust 的零成本抽象,鼓励使用迭代器代替循环;
第十四章 更多关于
Cargo 和 Crates.io 的内容
- 使用
[profile.dev]
后跟 opt-level = 1
开启优化,等级为 0~3;
- 使用
///
编辑文档注释,可以运行 cargo doc
使用 rustdoc 生成对应 HTML 文档并存入
target/doc;当文档注释中的例子与代码不同步或产生 panic 时
cargo test
会报错;
- 使用
//!
编辑包含此注释的 crate 或模块整体的文档;
- 使用 workspace 避免相互依赖的不同模块的重复构建;
第十五章 智能指针
- 智能指针实现了
Deref
和 Drop
trait,使用结构体和引用计数(reference counting)实现;
Rc<T>
允许相同数据有多个所有者;Box<T>
和 RefCell<T>
有单一所有者。
Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。
- 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T>
自身是不可变的情况下修改其内部的值。
- 使用
Box::new()
在堆上分配值;使用 Box
定义一个递归类型 cons list;
- 定义
Deref
trait:
1 2 3 4 5 6 7 8 9 10 11
| use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> { type Target = T;
fn deref(&self) -> &T { &self.0 } }
|
- 解引用强制转换(deref coercions):实现了
Deref
trait 的类型 A 可以根据实现内容,隐式转换为另一个类型
B 的引用;
- Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制转换:
- 当
T: Deref<Target=U>
时从 &T
到 &U
。
- 当
T: DerefMut<Target=U>
时从 &mut T
到 &mut U
。
- 当
T: Deref<Target=U>
时从 &mut T
到 &U
。
- 使用
Drop
trait 运行清理代码,但不能使用
drop
方法提前释放;应使用 std::mem::drop
,即使用 drop(a)
而非 a.drop()
;
Rc<T>
允许使用 Rc::clone(&ptr)
(仅增加引用计数而非深拷贝)共享所有权;可以通过
strong_count
和 weak_count
获得引用数;只允许不可变借用;
- 内部可变性(Interior mutability):允许使用
unsafe
代码模糊可变性和借用规则;
RefCell<T>
代表数据的唯一所有权,但在运行时才检查借用规则,它和
Rc<T>
均只能用于单线程场景;borrow
方法返回 Ref<T>
类型的智能指针,borrow_mut
方法返回 RefMut
类型的智能指针
- 结合二者来拥有多个可变数据的所有者:
1 2 3 4
| enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, }
|
- 调用
Rc::downgrade
得到 weak<T>
;对
weak<T>
使用 upgrade
得到
Option<Rc<T>>
;
Cell<T>
允许实现了 Copy
trait
的类型的内部可变性(允许在持有不可变引用时修改其内容),使用编译期检查;
第十六章 无畏并发
- Fearless Concurrency:Rust 坚持几乎没有运行时,且支持调用 C
语言,因此语言线程与 OS 线程比为 1:1;
- 调用
thread::spawn
并传递闭包,返回一个拥有所有权的
JoinHandle
,调用其 handle.join().unwrap()
时会等待线程结束;
- 使用
move
闭包强制闭包获取使用的值的所有权;
- 使用通道(Channel,思想来源于
Go)实现消息传递:使用
let (tx, rx) = mpsc::channel();
创建多个生产者,单个消费者通道;
- 通道会传递所有权,使用
tx.send()
发送,
rx.recv()
或迭代器接收;使用 tx.clone()
获取多生产者;
mutex<T>
使用 lock()
阻塞直到获取锁,返回一个 MutexGuard
智能指针;
- 锁的所有权无法移动到多个线程中,因此封装进
Arc<T>
原子引用计数类型中;
- 两个内嵌于语言中的并发概念:
std::marker
中的
Sync
(可以在多个线程中拥有其引用)和 Send
(所有权可以在线程间传递)trait;
Rc<T>
不支持 Send
和
Sync
trait,应使用 Arc<T>
;
RefCell<T>
和 Cell<T>
系列类型不是
Sync
的,而 Mutex<T>
是;
第十七章 Rust 的面向对象特性
- Rust
支持抽象、封装(但不使用对象的概念,数据和行为定义也是分开的),不支持完整的继承,使用
trait 实现多态;
- trait 实现的继承的不完全替代:
pub components: Vec<Box<dyn Draw>>
,这里实现了
Draw
trait
的类型可以有多种,而非与泛型一样只代表一种;trait 对象通过虚函数表
vtable 实现;
- trait 对象需要使用动态分发;返回值不为
Self
且方法没有泛型参数的 trait 才能组成 trait
对象,这称为对象安全(因为运行时已经忘记具体类型);
- 例:使用 trait 实现状态模式;
第十八章 模式和匹配
match
、 if let
、 while let
、 for
、 let
、函数传参均会使用模式;
- refutable:匹配可能失效,irrefutable:不可能失败,如
let
定义;
match
中 1..=5
等同于
1 | 2 | 3 | 4 | 5
;
1 2 3
| let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; let Point { x, y } = p;
|
id: id_variable @ 3..=7
匹配的同时绑定到变量上;
第十九章 高级特征
- 不安全的 Rust:
- 解引用裸指针;
- 调用不安全函数或方法:创建不安全代码的安全抽象、使用
extern
调用外部代码;
- 访问或修改可变静态变量;
- 实现不安全 trait;
- 访问
union
(一般用于和 C 交互)中的字段;
- 关联类型:用于类型占位,在实现时指定真正类型;
1 2 3 4 5
| impl Iterator for Counter { type Item = u32;
fn next(&mut self) -> Option<Self::Item> { ... } }
|
- 默认泛型类型参数和运算符重载:实现
+
重载的方式举例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| trait Add<RHS=Self> { type Output;
fn add(self, rhs: RHS) -> Self::Output; }
impl Add<Meters> for Millimeters { type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) } }
|
- 调用不同 trait 的同名方法时需要同时指定
trait::func()
;调用关联函数语法 struct::func()
;通过完全限定语法调用结构体的 trait
<Dog as Animal>::func()
;父 trait 使用了其它 trait
的功能: trait OutlinePrint: fmt::Display
;
- newtype 模式:在想添加功能的类型外套一层 wrapper类型:
struct Wrapper(Vec<String>)
,可以再通过实现
Deref
trait 返回内部类型实现和对待原类型一样对待此
wrapper;
- 类型别名:
type Kilometers = i32;
,
type Thunk = Box<dyn Fn() + Send + 'static>;
- 从不返回的 never type:
!
,可以强转为其它任何类型,continue、panic 等均为此类型,可以用于
match
;
str
是动态大小类型 DST(unsized types),
&str
存储两个值:地址和长度,因此它的大小是 2 *
usize
;可以使用 &str
、
Box<str>
或 Rc<str>
,但不能直接使用 str
,trait 对象同样如此;
?Sized
trait bound 与 Sized
相对,读作
“T
可能是也可能不是 Sized
的”,博人的泛型参数均为
Sized
;
fn(i32) -> i32
表示一个函数指针,函数指针实现了所有三个闭包
trait(Fn
、FnMut
和 FnOnce
);函数指针可以作为返回值,闭包不能直接作为返回值,因为它表现为
trait,是 unsized 的,可以将其封装进 Box
返回,或通过 impl
trait 返回:
1 2 3 4
| fn create_fn() -> impl Fn() { let text = "Fn".to_owned(); move || println!("This is a: {}", text) }
|
宏(Macros):
- 声明宏
macro_rules!
: #[macro_export]
使我们能够使用引入的 crate 中定义的宏(否则默认为私有);多个模式使用
;
分隔,例: vec![1, 2, 3]
的宏:
1 2 3 4 5 6 7 8 9 10 11 12
| #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; }
|
1 2 3 4
| use proc_macro;
#[some_attribute] pub fn some_name(input: TokenStream) -> TokenStream { ... }
|
附录:
- 加上
r#
前缀后可以使用 Rust 的关键字;
- 使用
_i
表明一个变量不会被使用(如循环变量);
- turbofish:
basket.values().sum::<u32>()
明确指定泛型参数;
cargo clippy
是一系列 lint
的集合,能更智能地观察代码;
Default
trait 允许使用 Struct::default()
构造默认结构体;
AsRef
允许引用间的转换,最常见的是对
&String
使用 as_ref()
转换成
&str
以同时兼容二者;
AsMut
从调用者取一个可变引用,如获取:
Box<T>
中的 &mut T
;
Vec<T>
中的 &mut [T]
;
String
中的 &mut str
;
Rc<RefCell<T>>
中的
RefMut<T>
;