本文参考自文档:Rust 程序设计语言 简体中文版,更多请看原始文档!
1. Hello world
2. Cargo(Rust 的构建系统和包管理器)
使用 Cargo 创建项目
Cargo 配置文件
Cargo 目录结构
构建并运行 Cargo 项目
发布(release)构建
Cargo 常用命令
cargo build
:构建项目cargo run
:一步构建并运行项目cargo check
:在不生成二进制文件的情况下构建项目来检查错误(比cargo run
快得多)
3. 【小项目】猜数游戏
准备一个新项目(工程目录)
使用 crate 来增加更多功能
Cargo.lock 文件确保构建是可重现的
更新 crate 到一个新版本
Code:猜数游戏
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
loop {
println!("请输入一个数字");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to get the number!");
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;
}
}
}
}
4. Rust 基础
4.1 变量和可变性
常量
隐藏(Shadowing)
4.2 数据类型
Rust 必须指定数据类型
标量类型
-
整型
-
浮点型(整数除法为地板除)
-
布尔型
-
字符型(size为4个字节;字符单引号
'
,字符串双引号"
)
复合类型
-
元组
-
数组
4.3 函数
-
参数
-
语句和表达式
- 语句不返回值;而表达式会计算出一个值
- 注意:表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值
3. 函数返回值
4.4 注释
//
4.5 控制流:if与循环
if 表达式
- 基础
- 在 let 语句中使用 if
loop 循环
- 基础
- 带标签的 loop 循环
- 用于在嵌套循环中 break 或 continue 指定循环
3. 从 loop 循环返回值
while 循环
for 循环(遍历集合)
5. 所有权
5.1 什么是所有权
栈(Stack)与堆(Heap)
所有权规则
变量作用域
String 类型
内存与分配
变量与数据交互的方式(一):移动(move,直接转移)
变量与数据交互的方式(二):克隆(clone,深复制)
只在栈上的数据:拷贝
所有权与函数
- 向函数传递值可能会移动或者复制:对于实现
copy
的是复制,否则为转移!
返回值与作用域
5.2 引用与借用
- 在任意给定时间,下面情况只能存在一种:1、要么 只能有一个可变引用;2、要么 只能有多个不可变引用
- 引用必须总是有效的(不能制造悬垂引用)
引用(&ref)
可变引用(&mut ref)
悬垂引用(Dangling References)
5.3 Slice 类型
引入
字符串 Slice
字符串字面值就是 slice(牢记此特性!)
字符串 slice 作为参数(而不是将 String 引用作为参数)
- 传入 slice 作为参数:
my_fn(&my_string[idx1..idx2])
其他类型的 slice
6. 结构体
6.1 结构体的定义和实例化
结构体定义、创建实例
创建实例:字段初始化简写语法
创建实例:结构体更新语法
- 注意:结构体中的“移动”(用到了任意一个未实现
copy trait
类型的数据)与“克隆”(只使用了实现copy trait
类型的数据)特性!!!
元组结构体
类单元结构体
结构体数据的所有权
6.2 结构体示例程序(打印结构体的内容,dbg)
示例:打印矩形的面积
通过派生 trait 来打印结构体的内容
6.3 方法(method)
6.3.1 定义方法
6.3.2 与结构体字段同名的方法
- 读取:
&self
- 修改:
&mut self
- 获取所有权:
self
6.3.3 带有多个参数的方法
6.3.4 多个 impl 块
6.3.5 关联函数
- 应用:(不是方法的)关联函数经常被用作返回一个结构体新实例的构造函数,这些函数的名称通常为
new
- 举例:在 String 类型上定义的
String::from
函数
6.4 小结
7. 枚举和模式匹配
7.1 枚举的定义
7.1.1 基本概念
7.1.2 枚举的简洁用法:构造函数
7.1.3 枚举的优势:处理不同类型和数量的数据
- 枚举成员的类型:字符串、数字类型、结构体、枚举
- 注意:在未将标准库枚举引入当前作用域中时,可以创建与标准库中同名的枚举!
7.1.4 在枚举中定义方法
7.2 Option 枚举
T
表示不会存在空值的情况;Option<T>
表示存在空值的情况,需要考虑对空值的处理Option<T>
和T
是不同类型的,无法直接进行计算(也就是说,在对Option<T>
进行运算之前必须将其转换为T
)Option
已经引入 prelude 中,无需前缀Option::
,可直接使用Some
、None
7.3 match 控制流结构
7.3.1 match 基础
match
的分支组成结构:匹配模式Coin::Penny
+=>
+ 代码块{some_code}
组成;match
可以有多个分支match
的匹配模式类型:字面值、变量、通配符、其他内容- 每个分支相关联的代码作为一个表达式,而表达式的结果值将作为整个
match
表达式的返回值 - 如果想要在分支中运行多行代码,可以使用大括号,而分支后的逗号是可选的
7.3.2 match 绑定值的模式
7.3.3 match 匹配 Option<T>
7.3.4 match 必须是穷尽的
7.3.5 match 中的通配模式和 _ 占位符
- 需要利用变量:通配模式
other => fn(other),
,且必须作为最后一个分支 - 不需要利用变量:
_
占位符,_ => fn(),
- 不需要利用变量,且不做任何事:
_ => (),
,返回单元值
7.4 if let 简洁控制流
if let
:只匹配一个模式的值而忽略其他模式的情况if let xxx else xxx
:匹配两个模式的值,并分别处理
7.5 小结
8. 包、Crate和模块管理
8.1 包和 Crate
8.1.1 基本概念
- crate 是 Rust 在编译时最小的代码单位;crate 有两种形式:二进制项(可以被编译为可执行程序)和库(没有 main 函数,也不会编译为可执行程序,而是提供一些诸如函数之类的东西,使其他项目也能使用这些东西)
- **包(package)**是提供一系列功能的一个或者多个 crate;包中可以包含至多一个库 crate(library crate),也可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)
8.2 模块的作用域与私有性
8.2.1 模块的相关概念
8.2.1 使用模块对相关代码进行分组
8.3 模块的路径
8.3.1 绝对路径与相对路径
8.3.2 使用 pub 关键字暴露路径
- 模块公有并不使其内容也是公有的:模块上的 pub 关键字只允许其父模块引用它,而不允许访问内部代码;模块是一个容器,只是将模块变为公有能做的其实并不太多,同时需要更深入地选择将一个或多个项变为公有(即添加
pub
前缀)
8.3.3 使用 super 起始的相对路径
8.3.4 创建公有的结构体和枚举
- 结构体:在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的(其字段默认未私有的,可以根据情况决定每个字段是否公有)
- 枚举:其成员默认就是公有的
8.4 使用 use 关键字将路径引入作用域
8.4.1 use 的作用域
- 注意:
use
只能创建use
当前所在的特定作用域内的短路径(比如:可用于在当前模块,切换到其他模块则需重新导入)
8.4.2 use 的使用习惯
- 使用
use
时,一般是引入一个模块(而不是直接引入某些函数),在调用函数时需要指定父模块
8.4.3 as 关键字
8.4.4 pub use:重导出名称
use some_mod
:仅在当前作用域生效,对外仍是私有的pub use some_mod
:不仅在当前作用域生效,还可导入到其他作用域生效
8.4.5 使用外部包
- 对于外部包,需要先在
Cargo.toml
文件中添加所需的包,再使用use
来导入包 - 对于标准库,则直接
use
导入包即可
8.4.6 使用 {} 嵌套路径来简化 use 的使用
- 举例:
use std::{cmp::Ordering, io};
、use std::io::{self, Write};
8.4.7 通过 glob 运算符将所有的公有定义引入作用域
8.5 将模块拆分成多个文件
9. 常见集合
9.1 Vector
9.1.1 创建 vector
- 创建空的 vector:
Vec::new()
- 创建有值的 vector:
vec!
宏
9.1.2 向 vector 中添加元素
9.1.3 读取 vector 的元素
- 索引语法:当引用一个不存在的元素时 Rust 会造成 panic
get
方法:当get
方法被传递了一个数组外的索引时,它不会 panic 而是返回None
,可后续用于match
进行对应的分支处理- 注意:不要同时读取并修改 vector!
9.1.4 遍历 vector 的元素
9.1.5 在 vector 中使用枚举来储存多种类型
9.1.6 丢弃 vector 时也会丢弃其所有元素
9.2 String
9.2.1 为什么字符串较为复杂?
9.2.2 什么是字符串
9.2.3 创建 String
String::new()
:创建一个空的 String"xxx".to_string()
:根据字符数据xxx创建一个 String(或者理解为向一个 String 中添加字符数据)String::from("xxx")
:根据字符数据xxx创建一个 String
9.2.4 更新字符串(一):向 String 中追加字符(串)
push
:获取一个单独的字符作为参数,并附加到 String 中push_str
:附加字符串 slice 到 String 中;此方法不获取参数的所有权
9.2.5 更新字符串(二):拼接 String
+
运算符:第一个参数必须使用原变量(该变量会被移动,夺去其所有权),后面的参数必须使用引用形式(若为&String
则会被强制转换为&str
)format!
宏:使用参数的引用,因此不会获取任何参数的所有权
9.2.6 Rust 的字符串不支持索引
9.2.7 字符串 slice
9.2.8 遍历字符串的方法
chars()
:拆分为单个字符,返回char
类型的值bytes()
:返回每一个原始字节
9.2.9 字符串并不简单
9.3 Hash Map
9.3.1 创建 Hash Map
- 与 vector 类似,hash map 是同质的:所有的键必须是相同类型,值也必须都是相同类型
HashMap::new()
:创建一个空的 hash map,使用insert(k, v)
方法往里面添加元素
9.3.2 访问 Hash Map
9.3.3 更新 Hash Map
- 直接覆盖一个值:最后一次更新的值为最终值
- 只在键没有对应值时插入键值对:
entry
与or_insert
方法,返回一个可变引用 - 根据旧值更新一个值:先获取旧值的可变引用,再更新(解引用
*
)
9.3.4 Hash Map 与所有权
- 对于有所有权的值,插入 hash map 后将会被移动;而对于像 i32 这样的实现了 Copy trait 的类型,其值是直接拷贝进 hash map
- 如果将值的引用插入哈希 map,这些值本身将不会被移动进 hash map (但是这些引用指向的值必须至少在 hash map 有效时也是有效的)
9.3.5 哈希函数
9.4 小结
10. 错误处理(Rust 没有异常)
10.1 处理不可恢复的错误:panic!
10.1.1 什么是 panic!
10.1.2 panic! 的 backtrace
- 阅读 backtrace 的关键:从头开始读直到发现你编写的文件(这就是问题的发源地),这一行往上是你的代码所调用的代码,往下则是调用你的代码的代码
10.2 处理可恢复的错误:Result
10.2.1 什么是 Result
Result<T, E>
:该枚举含有T
和E
两个成员,T
代表成功时返回的Ok
成员中的数据的类型,E
代表失败时返回的Err
成员中的错误的类型Result
通常配合match
使用
10.2.2 Result 配合 match 来匹配不同的错误
10.2.3 Result 与闭包等方法的结合(简化 match)
10.2.4 失败时 panic 的简写:unwrap 和 expect
-
unwrap
:提供默认的 panic! 信息;expect
:提供自定义的错误信息 -
expect
比unwrap
使用更多 -
10.2.5 传播错误
- 传播错误:遇到错误时可提前返回,并结束函数
10.2.6 传播错误的简写:? 运算符
?
运算符与match
的工作方式类似:若Result
的值为Ok
,则正常返回值且程序继续执行;若Result
的值为Err
,则将Err
中的值将作为整个函数的返回值,提前结束函数?
运算符与match
的不同点:?
运算符所使用的错误值被传递给了from
函数,它定义于标准库的From
trait 中,其用来将错误从一种类型转换为另一种类型(收到的错误类型被转换为由当前函数返回类型所指定的错误类型,可自定义实现错误类型,如:OurError
)?
运算符可使用链式方法调用
10.2.7 ? 运算符的使用场景
?
运算符只能被用于返回值与?
运算符作用的值相兼容的函数?
运算符的适用返回值类型:Result
、Option<T>
、实现了FromResidual
的返回值函数?
运算符遇到错误时常用的修改方法:1、修改函数的返回值类型;2、使用match
或Result<T, E>
的方法来处理- 注意:可以在返回
Result
的函数中对Result
使用?
运算符,可以在返回Option
的函数中对Option
使用?
运算符,但是不可以混合搭配。?
运算符不会自动将Result
转化为Option
,反之亦然;在这些情况下,可以使用类似Result
的ok
方法或者Option
的ok_or
方法来显式转换
10.3 使用 panic! 还是返回 Result
10.3.1 使用 panic! 的场景
10.3.2 返回 Result 的场景
10.3.3 错误处理指导原则
10.3.4 创建自定义类型进行有效性验证
10.4 小结
11. 泛型、Trait 和生命周期
11.1 泛型数据类型
11.1.1 函数的泛型
- 注意:泛型的比较适用于实现了
std::cmp::PartialOrd
trait 的数据类型
11.1.2 结构体的泛型
- 结构体的泛型可以使用多个泛型类型参数
11.1.3 枚举的泛型
- 类似于结构体,枚举的泛型也可以使用多个泛型类型参数
11.1.4 方法的泛型
- 定义方法时可以为泛型指定限制(只有指定类型的数据可以使用该方法)
- 结构体定义的泛型参数可以与结构体方法签名的泛型参数不同,从而可以交叉使用不同的泛型类型
11.1.5 泛型代码的性能
11.2 Trait:定义共同行为
11.2.1 定义 trait
trait
定义了不同类型的共享功能trait
中可以只提供方法签名(后跟分号),而提供具体的实现是可选的trait
的具体实现由每个实现该trait
的类型来定义并提供,且每个实现都使用相同的方法签名trait
体中可以有多个方法,一行一个方法签名且都以分号结尾
11.2.2 实现 trait
impl Xxx_trait for Yyy
:为 Yyy 类型实现 Xxx_trait(使用for
)- 实现
trait
的限制:只有当至少一个trait
或者要实现trait
的类型位于 crate 的本地作用域时,才能为该类型实现trait
;不能为外部类型实现外部trait
11.2.3 trait 的默认实现
- 默认实现是可选的
- 默认实现允许调用相同
trait
中的其他方法,哪怕这些方法没有默认实现(也就是只需重新实现对应部分即可)
11.2.4 trait 作为参数(一):impl Trait 语法
- impl Bound 语法用于(不)相同类型的参数
11.2.5 trait 作为参数(二):Trait Bound 语法
- trait bound 语法用于相同类型的参数
trait_1 + trait_2 + ...
:实现多个不同trait
相加(同时实现)- 可通过
where
从句简化 trait bound
11.2.6 返回实现了 trait 的类型
11.2.7 使用 trait bound 有条件地实现方法
- 可以有条件地只为那些实现了特定
trait
的类型来实现方法、trait
- 举例:带【Display】
trait
条件实现【xxx】方法fn
:impl<T: Display> Pair<T> { fn xxx() }
- 举例:带【Display】
trait
条件实现【ToString】trait
:impl<T: Display> ToString for T
11.3 生命周期:确保引用是有效的
11.3.1 悬垂引用与借用检查器
- 生命周期避免了悬垂引用
11.3.2 函数中的泛型生命周期
11.3.3 生命周期注解语法
- 生命周期注解并不改变任何引用的生命周期的长短,而是用于描述了多个引用生命周期相互的关系
- 生命周期注解:生命周期参数名称以撇号(')开头,其名称通常全是小写
&i32
:引用&'a i32
:带有显式生命周期的引用&'a mut i32
:带有显式生命周期的可变引用
11.3.4 函数签名中的生命周期注解
- 函数签名中的生命周期注解:在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样
- 当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中
- 类似于函数
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
中,泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个(核心概念!)
11.3.5 函数返回引用时需要关联生命周期
- 注意:若返回引用,则需要将多个参数与其返回值的生命周期进行关联,否则最好返回有所有权的值!
11.3.6 结构体中的生命周期注解
- 定义包含引用的结构体:需要为结构体定义中的每一个引用添加生命周期注解
11.3.7 生命周期省略(Lifetime Elision)
- 生命周期省略规则(一):编译器为每一个(输入、输出)引用参数都分配了一个生命周期参数
- 生命周期省略规则(二):如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
- 生命周期省略规则(三):如果方法有多个输入生命周期参数并且其中一个参数是
&self
或&mut self
,那么所有输出生命周期参数被赋予 self 的生命周期(该规则只适用于方法fn
中)
11.3.8 方法(fn)中的生命周期注解
- 实现带有生命周期的结构体实现方法
fn
时,类似于泛型类型参数的语法,比如:impl<'a> xxx_struct<'a>
- 根据生命周期省略(三),无需在方法
fn
签名中使用生命周期注解
11.3.9 静态生命周期
- 静态生命周期
'static
:其生命周期能够存活于整个程序期间 - 所有的字符串字面值都拥有
'static
生命周期 - 使用
'static
前,思考这个引用是否真的在整个程序的生命周期里都有效
11.3.10 同时使用:泛型类型参数、trait bounds 和生命周期
11.4 小结
12. 自动化测试
12.1 编写测试
12.1.1 测试函数
- 测试函数:在一个函数前加上一行
#[test]
注解将普通函数变成测试函数
12.1.2 assert! 宏
12.1.3 assert_eq! 与 assert_ne!
assert_eq!(left, right)
与assert_eq!(left, right)
在失败时会返回left
与right
两个值,比assert!(xxx)
传递的信息更完善- 使用
assert_eq!
与assert_eq!
宏的值必须实现PartialEq
(用于断言两个值是否相等)与Debug
(在断言失败时打印他们的值)这两个派生trait
- 对于自定义的结构体与枚举,通常可以添加
#[derive(PartialEq, Debug)]
注解,来使用assert_eq!
与assert_eq!
12.1.4 在断言中自定义失败信息
- 在
assert!
、assert_eq!
与assert_ne!
宏中,除了必需参数外,后面所有的参数都会传递给format!
宏,作为失败时的输出进行打印
12.1.5 使用 should_panic 检查 panic
should_panic
:在函数中的代码panic
时会通过,而在其中的代码没有panic
时失败。should_panic
属性有一个可选的expected
参数:测试工具会确保错误信息中包含其提供的文本。#[should_panic(expected = "Some text for debug...")]
12.1.6 使用 Result<T, E> 编写测试
- 使用
Result<T, E>
编写测试,在通过时返回Ok
成员,在失败时返回Err
成员 - 不能对这些使用
Result<T, E>
的测试使用#[should_panic]
注解 - 为了断言一个操作返回
Err
成员,不要使用对Result<T, E>
值使用问号表达式(?),而是使用assert!(value.is_err())
12.2 控制测试如何运行
12.2.1 命令行参数分别传递
cargo test --help
cargo test -- --help
12.2.2 并行或连续的运行测试
$ cargo test -- --test-threads=1
12.2.3 显示函数输出
$ cargo test -- --show-output
12.2.4 运行指定的一部分测试(一):运行单个测试
cargo test xxx_fn
:指定xxx_fn
方法运行;- 注意:不能指定多个测试名称,只有传递给 cargo test 的第一个值才会被使用
12.2.5 运行指定的一部分测试(二):运行多个测试
cargo test xxx
:指定所有带有xxx
方法运行- 此外,也可以通过指定模块名来运行一个模块中的所有测试
12.2.6 忽略某些测试
- 若不想执行某个测试,则在前面加上一行
#[ignore]
来忽略此测试 cargo test -- --ignored
:只运行被忽略的测试cargo test -- --include-ignored
:运行全部测试(不管是否存在忽略的测试)
12.3 测试的组织结构
12.3.1 单元测试
- 单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中,具体规范为在每个文件中创建包含测试函数的
mod tests
模块,并且使用#[cfg(test)]
注解来标注模块
12.3.2 集成测试(一):tests 目录
12.3.3 集成测试(二):子模块
tests
目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中(即将公共部分放在一个子文件夹中)
12.3.4 集成测试(三):二进制 crate
12.4 小结
13. 【项目】构建一个简易的命令行程序 grep
13.1 接受命令行参数
13.1.1 读取参数值
env::args().collect()
:将获取的命令行参数,形成一个集合
13.1.2 将参数值保存进变量
13.2 读取文件
13.3 代码重构
13.3.1 二进制项目的关注分离
main.rs
只需处理程序运行:无法直接测试 main 函数,且保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性lib.rs
处理所有的真正的任务逻辑:将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试它们
13.3.2 项目重构(一):提取参数解析器
13.3.3 项目重构(二):组合配置值
13.3.4 项目重构(三):创建一个 Config 的构造函数
13.3.5 项目重构(四):修复错误处理
13.3.6 项目重构(五):从 main 提取逻辑
13.3.7 项目重构(六):将代码拆分到库 crate
13.4 采用测试驱动开发(TDD)完善库的功能
13.4.1 编写失败测试
13.4.2 编写使测试通过的代码
13.5 处理环境变量
13.5.1 编写一个大小写不敏感 search 函数的失败测试
13.5.2 实现 search_case_insensitive 函数
13.6 将错误信息输出到标准错误(stderr)而不是标准输出(stdout)
13.6.1 检查错误应该写入何处
- 目的:期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息
13.6.2 将错误打印到标准错误(stderr)
eprintln!
宏可以打印错误到标准错误(stderr)
14. Rust 中的函数式语言功能:迭代器与闭包
14.1 闭包:捕获环境的匿名函数
14.1.1 闭包会捕获其环境
14.1.2 闭包类型推断和注解
- 闭包并不总是要求像
fn
函数那样在参数和返回值上注明类型 - 闭包通常很短,并只关联于小范围的上下文而非任意情境
- 如果尝试对同一闭包使用不同类型则就会得到类型错误!
14.1.3 捕获引用或者移动所有权
- 闭包可以有3种参数捕获方式:1、不可变借用;2、可变借用;3、获取所有权
- 对于不可变借用:可多处使用(如:打印)
- 对于可变借用:在闭包定义和调用之间不能有不可变引用来使用(如:打印)!
- 对于获取所有权:可使用
move
关键字来强制闭包获取它用到的环境中值的所有权
14.1.4 将被捕获的值移出闭包和 Fn trait
- 闭包可以做3种事:1、将一个捕获的值移出闭包;2、修改捕获的值,但不移出闭包;3、既不移动也不修改值,或者一开始就不从环境中捕获值。与之对应的,闭包取决于应用场景可以实现下面3种 trait(可同时实现多个 trait)
FnOnce
:适用于能被调用一次的闭包,所有闭包都至少实现了这个 trait,因为所有闭包都能被调用。一个会将捕获的值移出闭包体的闭包只实现FnOnce
trait,这是因为它只能被调用一次(比如:xxx_vec.push(value)
将会移出value
的所有权给到闭包外部的向量xxx_vec
,因此该闭包就会被实现为一个FnOnce
闭包)FnMut
:适用于不会将捕获的值移出闭包体的闭包,但它可能会修改被捕获的值。这类闭包可以被调用多次(==闭包内部可以做一些其他的计算操作 或者说 修改值,但这些操作不能返回值 或者说 不能将值移出闭包体,比如示例13-9所示的操作:num_sort_operations += 1;
==)Fn
:适用于既不将被捕获的值移出闭包体也不修改被捕获的值的闭包,当然也包括不从环境中捕获值的闭包。这类闭包可以被调用多次而不改变它们的环境,这在会多次并发调用闭包的场景中十分重要- 注意:上面这些 trait 会根据代码的实现方式来自动对应!!!
14.2 迭代器:处理元素序列
14.2.1 Rust 的迭代器
- 在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果
14.2.2 Iterator trait 和 next 方法
Iterator
trait 要求同时定义一个 Item 类型,这个 Item 类型被用作next
方法的返回值类型- 迭代器的消费(重要概念):在迭代器上调用
next
方法改变了迭代器中用来记录序列位置的状态,每调用一次next
都会从迭代器中消费(去掉)一个项 iter
:调用中得到的值是不可变引用iter_mut
:调用中得到的值是可变引用into_iter
:获取所有权并返回拥有所有权的迭代器
14.2.3 消费适配器:调用 next 方法会消费迭代器
- 迭代器的消费(重要概念):在迭代器上调用
next
方法改变了迭代器中用来记录序列位置的状态,每调用一次next
都会从迭代器中消费(去掉)一个项 - 调用
next
方法的方法被称为 消费适配器(consuming adaptors),因为调用这些方法会获取迭代器的所有权并反复调用next
来遍历迭代器(也就是会消耗迭代器)
14.2.4 迭代适配器:改变迭代器类型
- Iterator trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),他们不消耗迭代器,而是将当前迭代器变为不同功能的迭代器(如:通过
map
方法来转换一个迭代器) - 可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果(如:
collect()
方法)!
14.2.5 使用闭包来获取上下文环境
filter
:判断一个使用迭代器的每一个项并返回布尔值的闭包,如果闭包返回 true,其值将会包含在filter
提供的新迭代器中。如果闭包返回 false,其值不会包含在结果迭代器中
14.2.6 实现 Iterator trait 来创建自定义迭代器
14.3 改进 I/O 项目
14.3.1 使用迭代器并去掉 clone
14.3.2 使用迭代适配器来使代码更简明
14.4 性能对比:循环 VS 迭代器
14.5 小结
15. 进一步认识 Cargo 和 Crates.io
15.1 采用发布配置自定义构建
15.2 将 crate 发布到 Crates.io
15.2.1 文档注释
未完待续。。。
15.3 Cargo 工作空间
15.3.1 创建工作空间
15.3.2 在工作空间中创建第二个包
15.4 使用 cargo install 安装二进制文件
15.5 Cargo 自定义扩展命令
15.6 小结
16. 智能指针
- 智能指针(smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能
- 引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有他们指向的数据
16.1 Box<T>:指向堆上的数据
box
允许将一个值放在堆上而不是栈上,box
是一个本身留在栈上但指向堆数据的指针box
的作用:提供固定大小、提供堆分配、间接存储
16.1.1 使用 Box<T> 在堆上储存数据
box
:在离开作用域时,将被释放,释放过程作用于box
本身(位于栈上)和它所指向的数据(位于堆上)
16.1.2 Box 的应用:创建递归类型
16.1.3 计算非递归类型的大小
enum
:实际上只会使用其中的一个成员,所以枚举值所需的空间等于储存其最大成员的空间大小
16.1.4 使用 Box<T> 给递归类型一个已知的大小
Box<T>
是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变- 核心思想:建议中的 “indirection” 意味着不同于直接储存一个值,应该间接的储存一个指向值的指针!
- 通过使用
box
,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了
16.2 通过 Deref trait 将智能指针当作常规引用处理
- 实现
Deref
trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针
16.2.1 解引用 *:追踪指针的值
- 解引用运算符
*
可用于追踪引用&
所指向的值
16.2.2 像引用一样使用 Box<T>
Box<T>
可用于生成拷贝一个值的引用(注意是引用,而不是值)的实例(而不是指向该值的引用,因为生成的是Box
引用,不是常规引用,二者不可比)
16.2.3 自定义智能指针(一):引入
16.2.4 自定义智能指针(二):实现 Deref trait 将某类型像引用一样处理
- 对于实现
Deref
trait 的数据类型,要求实现deref
方法,该方法借用self
并返回一个内部数据的引用、并可用于后续的解引用 - 没有
Deref
trait 的话,编译器只会解引用&
引用类型 *y
的底层运行为*(y.deref())
:Rust 将*
运算符替换为先调用deref
方法再进行普通解引用的操作
16.2.5 函数和方法的隐式 Deref 强制转换
- Deref 强制转换(deref coercions)将实现了 Deref trait 的类型的引用转换为另一种类型的引用
- 当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行,这时会有一系列的 deref 方法被调用,把我们提供的类型转换成了参数所需的类型
16.2.6 Deref 强制转换如何与可变性交互
Deref
trait 重载不可变引用的*
运算符;DerefMut
trait 用于重载可变引用的 * 运算符- Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换,如下所示:
-
- 当
T: Deref<Target=U>
时从&T
到&U
:如果有一个&T
,而T
实现了返回U
类型的 Deref,则可以直接得到&U
- 当
-
- 当
T: DerefMut<Target=U>
时从&mut T
到&mut U
:与 1 类似(同上)
- 当
-
- 当
T: Deref<Target=U>
时从&mut T
到&U
:Rust 也会将可变引用强转为不可变引用,但与之相反的,不可变引用永远也不能强转为可变引用!
- 当
16.3 Drop Trait:运行清理代码
16.3.1 Drop Trait 的基本概念
16.3.2 std::mem::drop:提前丢弃值
std::mem::drop
可显式调用来丢弃值,该方法已经存在于 prelude 中- 所有权系统确保引用总是有效的,也会确保
drop
只会在值不再被使用时被调用一次
16.4 Rc<T>:引用计数智能指针
- 引用计数(reference counting):意味着记录一个值引用的数量来知晓这个值是否仍在被使用,如果某个值有零个引用,就代表没有任何有效引用并可以被清理
- 如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效
- 为了启用多所有权需要显式地使用 Rust 类型
Rc<T>
,注意Rc<T>
只能用于单线程场景
16.4.1 使用 Rc<T> 共享数据
Rc<T>
可通过克隆的方式Rc::clone
来增加引用计数,直到有零个引用之前其数据都不会被清理Rc::clone
的实现并不像大部分类型的clone
实现那样对所有数据进行深拷贝,只会增加引用计数,这并不会花费多少时间,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆(在下面的示例中也可以调用a.clone()
)
16.4.2 克隆 Rc 会增加引用计数
Rc::strong_count
:返回引用计数的值
16.5 RefCell<T> 和内部可变性模式
- 内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用
unsafe
代码来模糊 Rust 通常的可变性和借用规则
16.5.1 RefCell<T>:在运行时检查借用规则
- 对于引用和
Box<T>
:借用规则的不可变性作用于编译时;如果违反这些规则会得到一个编译错误 - 对于
RefCell<T>
:这些不可变性作用于运行时;而对于RefCell<T>
,如果违反这些规则程序会panic
并退出
16.5.2 内部可变性:不可变值的可变借用
- 借用规则推论:对于一个不可变值,不能可变的借用它
16.5.3 内部可变性的示例:mock 对象
std::cell::RefCell
:内部可变性相关模块- 在下面的示例中,对于要修改的结构体字段
sent_messages
字段的类型是RefCell<Vec<String>>
而不是Vec<String>
;在 new 函数中新建了一个RefCell<Vec<String>>
实例替代空vector
RefCell::borrow_mut
方法来获取 RefCell 中值的可变引用;RefCell::borrow
方法来获取 RefCell 中值的不可变引用
16.5.4 RefCell<T>:在运行时记录借用
- 不可变引用:一般使用
&
语法;对于RefCell<T>
使用borrow
方法,返回一个Ref<T>
类型的智能指针,且实现了Deref
trait - 可变引用:一般使用
&mut
语法;对于RefCell<T>
使用borrow_mut
方法,返回一个RefMut<T>
类型的智能指针,且实现了Deref
trait RefCell<T>
记录当前有多少个活动的Ref<T>
和RefMut<T>
智能指针- 每次调用
borrow
,RefCell<T>
将活动的不可变借用计数加一;当Ref<T>
值离开作用域时,不可变借用计数减一 - 像编译时借用规则一样,
RefCell<T>
在任何时候只允许有多个不可变借用或一个可变借用 - 如果违反借用规则,相比引用时的编译时错误,
RefCell<T>
的实现会在运行时出现panic
16.5.5 结合 Rc<T> 和 RefCell<T> 来拥有多个可变数据所有者
16.6 引用循环与内存泄漏
16.6.1 制造引用循环
- 创建引用循环并不容易,但也不是不可能:如果你有包含
Rc<T>
的RefCell<T>
值或类似的嵌套结合了内部可变性和引用计数的类型,请小心检查有没有形成一个引用循环 - 另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有(换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃)
- 代码解析:
*link.borrow_mut() = Rc::clone(&b);
表示将取出的变量link
引用通过borrow_mut()
方法来获得其可变引用,再用*
来解引用指针,解引用后将这个可变引用重新指向 b 的克隆引用(*link.borrow_mut()
应该等价于*(link.borrow_mut())
)
16.6.2 避免引用循环:将 Rc<T> 变为 Weak<T>
- 创建弱引用(weak reference):调用
Rc::downgrade
并传递Rc<T>
实例的引用来创建其值的弱引用,并会得到Weak<T>
类型的智能指针,会将weak_count
加 1。创建弱引用的例子:*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
- 弱引用的
weak_count
:用来记录存在多少个Weak<T>
引用,与strong_count
的区别在于,weak_count
无需计数为 0 就能使Rc<T>
实例被清理 - 使用
upgrade
方法用于判断指向的值是否被丢弃:因为Weak<T>
引用的值可能已经被丢弃了,为了使用Weak<T>
所指向的值,我们必须确保其值仍然有效,为此可以调用Weak<T>
实例的upgrade
方法,这会返回Option<Rc<T>>
- 强、若引用的区别:强引用代表如何共享
Rc<T>
实例的所有权(引用并拥有),但弱引用并不属于所有权关系(仅引用,但不拥有)! - 弱引用不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断
16.7 小结
17. Rust 中的并发
17.1 线程
17.1.1 spawn:创建新线程
thread::spawn
:创建一个新线程,需要传递一个闭包,并在其中包含希望在新线程运行的代码thread::sleep
:调用强制线程停止执行一小段时间。比如:thread::sleep(Duration::from_millis(1));
17.1.2 join:等待所有线程结束
thread::spawn
的返回值类型是JoinHandle
,JoinHandle
是一个拥有所有权的值JoinHandle.join
:通过调用handle
的join
会阻塞当前线程直到handle
所代表的线程结束(也就是等待所关联的线程运行结束)join
的调用位置会影响线程的运行顺序(比如:影响线程是否同时运行)
17.1.3 线程与 move 闭包
- 可以在参数列表前使用
move
关键字强制闭包获取其使用的环境值的所有权。比如:使用main
函数中的外部数据,这样就无法知道这些外部数据的生命周期,因此无法编译 move
关键字经常用于传递给thread::spawn
的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程move
关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则
17.2 使用消息传递在线程间传送数据
17.2.1 消息传递(message passing)与信道(channel)
use std::sync::mpsc;
:mpsc
是 多个生产者,单个消费者(multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 发送(sending)端,但只能有一个消费这些值的 接收(receiving)端mpsc::channel
函数返回一个元组:第一个元素是发送端(发送者,tx),而第二个元素是接收端(接收者,rx,具有迭代器特性)- 发送者方法:
send
(转移所有权) send
(转移所有权):用来获取需要放入信道的值;该方法返回一个 Result<T, E> 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误- 接收者方法:
recv
、try_recv
recv
:该方法会阻塞主线程执行直到从信道中接收一个值(即:一直等待消息,直到接收到一个值才会结束)。一旦发送了一个值,recv
会在一个Result<T, E>
中返回它。当信道发送端关闭,recv
会返回一个错误表明不会再有新的值到来了try_recv
:不会阻塞,相反它立刻返回一个 Result<T, E>:Ok 值包含可用的信息,而 Err 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv 很有用
17.2.2 信道与所有权转移
send
:函数获取其参数的所有权并移动这个值归接收者所有
17.2.3 发送多个值并观察接收者的等待
- 接收者
rx
是一个迭代器
17.2.4 通过克隆发送者来创建多个生产者
- 注意:下面的例子,是不确定顺序的实现!可以添加
join
来阻塞线程,进而控制接收消息的先后顺序
17.3 共享状态并发
17.3.1 互斥器一次只允许一个线程访问数据
- 互斥器(mutex)在任意时刻只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过**获取互斥器的 锁(lock)**来表明其希望访问数据
17.3.2 互斥器 Mutex<T>
use std::sync::Mutex;
Mutex<T>
:使用关联函数new
来创建一个Mutex<T>
,Mutex<T>
是一个智能指针;Mutex<T>
提供了内部可变性,就像 Cell 系列类型那样lock
:获取锁,以访问互斥器中的数据,返回一个叫做MutexGuard
的智能指针(这个智能指针实现了Deref
来指向其内部数据;其也提供了一个Drop
实现当MutexGuard
离开作用域时自动释放锁)。这个调用会阻塞当前线程,直到我们拥有锁为止- 锁的特性,如下所示
- 如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。在这种情况下,没人能够再获取锁(所以这里可以选择
unwrap
并在遇到这种情况时使线程panic
) - 一旦获取了锁,就可以将返回值视为一个其内部数据的可变引用了(在示例中,
m
的类型是Mutex<i32>
而不是i32
,所以必须获取锁才能使用这个i32
值)
17.3.3 在线程间共享 Mutex<T>(一):不能将锁的所有权移动到多个线程中
17.3.4 在线程间共享 Mutex<T>(二):多线程和多所有权
Rc<T>
并不能安全的在线程间共享
17.3.5 在线程间共享 Mutex<T>(三):原子引用计数 Arc<T>
Arc<T>
:原子引用计数(atomically reference counted)类型,一个类似Rc<T>
并可以安全的用于并发环境的类型。Arc<T>
和Rc<T>
有着相同的 APIMutex<T>
提供了内部可变性,就像 Cell 系列类型那样,因此如同使用RefCell<T>
可以改变Rc<T>
中的内容那样,同样的可以使用Mutex<T>
来改变Arc<T>
中的内容
17.3.6 RefCell<T>/Rc<T> 与 Mutex<T>/Arc<T> 的相似性
17.4 使用 Sync 和 Send trait 的可扩展并发
17.4.1 通过 Send 允许在线程间转移所有权
Send
trait(所有权):一个实现了Send
的类型值的所有权可以在线程间传送Send
的类型:几乎所有的 Rust(基本)类型都是Send
的、完全由Send
的类型组成的类型也会自动被标记为Send
、Arc<T>
- 非
Send
的类型::Rc<T>
、裸指针(raw pointer)
17.4.2 Sync 允许多线程访问
Sync
trait(引用):一个实现了Sync
的类型可以安全的在多个线程中拥有其值的引用Sync
的类型:基本类型是Sync
的、完全由Sync
的类型组成的类型也是Sync
的、Mutex<T>
- 非
Sync
的类型:Rc<T>
、RefCell<T>
、Cell<T>
17.4.3 手动实现 Send 和 Sync 是不安全的
- 注意:通常并不需要手动实现
Send
和Sync
trait!
17.5 小结
18. Rust 的面向对象特性
18.1 面向对象语言的特征
18.1.1 对象:数据 + 行为
18.1.2 封装隐藏了实现细节
- 在 Rust 中,在代码中不同的部分考虑使用
pub
可以封装其实现细节
18.1.3 继承,作为类型系统与代码共享
- 在 Rust 中,不存在继承的机制,而是使用 trait 对象来提供相关的功能
18.2 trait 对象
- 已知全部的数据类型时,可以使用固定集合(枚举);但当全部的数据类型不是已知时(会动态变化),就需要使用 trait 对象来实现了
18.2.1 trait 对象(是一个实例):定义通用行为
trait
对象:指向一个实现了指定trait
类型的实例,以及一个用于在运行时查找该类型的trait
方法的表- 通过指定某种**指针(
&
引用或Box<T>
智能指针,还有dyn
关键字)**来创建trait
对象 - 可以使用
trait
对象代替泛型或具体类型,无需在编译时就知晓所有可能的类型 trait
对象将数据和行为两者相结合(不同于传统的对象,不能向trait
对象增加数据),trait
对象的作用是允许对通用行为进行抽象- 对比泛型类型:泛型类型参数一次只能替代一个具体类型,而
trait
对象则允许在运行时替代多种具体类型
18.2.2 实现 trait
- 将一个或多个类型的实例,放入
Box<T>
中就可以转换得到trait
对象! - 使用
trait
对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现trait
对象所需的trait
则 Rust 不会编译这些代码
18.2.3 trait 对象执行动态分发
18.2.4 trait 对象需要类型安全
- 只有**对象安全(object-safe)**的 trait 可以实现为特征对象
- 如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:1、返回值不是 Self;2、没有泛型类型的参数
- 核心原因:一旦使用 trait 对象,Rust 将不再知晓该实现的返回类型!
18.3 面向对象设计模式的实现
18.3.1 状态模式
- 状态模式(state pattern):是一个面向对象设计模式,该模式的关键在于定义一系列值的内含状态,这些状态体现为一系列的状态对象,同时值的行为随着其内部状态而改变
- 状态对象共享功能
- 每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。而持有一个状态对象的值,对于不同状态的行为以及何时状态转移毫不知情
18.3.2 博客项目(一):定义 Post 并新建一个草案状态的实例
18.3.3 博客项目(二):存放博文内容的文本
- 对于需要修改的字段需要传入可变引用
&mut self
18.3.4 博客项目(三):确保博文草案的内容是空的
18.3.5 博客项目(四):请求审核博文来改变其状态
- 代码解析:
if let Some(s) = self.state.take()
表示先取出当前state
的值及其所有权,再给到中间变量s
,s
再根据实际获取到的state
值调用对应的request_review
方法(即判断是属于Draft
还是PendingReview
的request_review
) - 在这里来看看状态模式的优势:无论 state 是何值,Post 的 request_review 方法都是一样的。每个状态只负责它自己的规则
18.3.6 博客项目(五):增加改变 content 行为的 approve 方法
- 调用
Option
的as_ref
方法是因为需要Option
中值的引用而不是获取其所有权 - 在下面的示例中,获取到的
&Box<dyn State>
,当调用其 content 时,Deref 强制转换会作用于&
和Box
- 在下面的示例中,获取 post 的引用作为参数,并返回 post 一部分的引用,所以返回的引用的生命周期与 post 参数相关
18.3.7 状态模式的权衡取舍
- 状态模式的优点:Post 的方法和使用 Post 的位置无需
match
语句,同时增加新状态只涉及到增加一个新struct
和为其实现trait
的方法 - 状态模式的缺点:因为状态实现了状态之间的转换,一些状态会相互联系;会有一些重复的逻辑(可通过默认方法、定义宏等方法来解决)
18.3.8 博客项目-改造(一):将状态和行为编码为类型
18.3.9 博客项目-改造(二):实现状态转移为不同类型的转换
18.4 小结
19. 模式与模式匹配
19.1 所有可能会用到模式的位置
19.1.1 match 分支
19.1.2 if let 条件表达式
- 可以组合并匹配
if let
、else if
和else if let
表达式,优势在于可以将多个值与模式比较(match
表达式一次只能将一个值与模式比较),且各个分支并不要求其条件相互关联 if let
也可以像match
分支那样引入覆盖变量if let
表达式的缺点在于其穷尽性没有为编译器所检查(不保证覆盖的完备性),而match
表达式则检查了(保证覆盖的完备性)
19.1.3 while let 条件循环
19.1.4 for 循环
for
可以获取一个模式:模式是for
关键字直接跟随的值,正如for x in y
中的x
19.1.5 let 语句
- 如果希望忽略元组中一个或多个值,也可以使用
_
或..
19.1.6 函数参数
19.2 Refutability(可反驳性): 模式是否会匹配失效
- 不可反驳的(irrefutable):能匹配任何传递的可能值的模式
- 可反驳的(refutable):对某些可能的值进行匹配会失败的模式
- 在不可反驳模式的地方使用可反驳模式通常是意义不大的
- 例子:
match
匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式(其实也可以在只有一个匹配分支的match
中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的let
语句替代)
19.3 所有的模式语法
19.3.1 匹配字面值
19.3.2 匹配命名变量
- 变量覆盖问题:存在外部变量的,则会优先使用外部变量;在新作用域中的新变量(会忽略外部的同名变量)会匹配任何
Some
中的值!
19.3.3 match 中的多个模式(|)
- 在
match
表达式中,可以使用|
语法匹配多个模式,它代表 或(or)的意思
19.3.4 通过 ..= 匹配值的范围
..=
语法允许你匹配一个闭区间范围内的值,但仅适用于数字或char
值(仅这两种类型可以判断范围是否为空的类型)
19.3.5 解构并分解值(一):解构结构体
- 匹配结构体字段的简写模式(1):只需列出结构体字段的名称,则模式创建的变量会有相同的名称
- 匹配结构体字段的简写模式(2):也可以使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量(这允许我们测试一些字段是特定值,同时会创建其他字段的变量)
19.3.6 解构并分解值(二):解构枚举
19.3.7 解构并分解值(三):解构嵌套的结构体和枚举
19.3.8 解构并分解值(四):解构结构体和元组
19.3.9 忽略模式中的值(一):使用 _ 忽略整个值
_
:作为匹配但不绑定任何值的通配符模式,可以将其用于任意模式。通常作为match
表达式最后的分支,也可以用作函数参数中
19.3.10 忽略模式中的值(二):使用嵌套的 _ 忽略部分值
- 可以在一个模式内部使用
_
忽略部分值 - 可以在一个模式中的多处使用
_
来忽略特定值
19.3.11 忽略模式中的值(三):通过在名字前以一个下划线开头来忽略未使用的变量
- 告诉 Rust 不要警告未使用的变量:可以用下划线作为变量名的开头(比如:
_x
) - 注意:下划线开头的变量
_x
仍会将值绑定到变量,而_
则完全不会绑定!
19.3.12 忽略模式中的值(四):用 … 忽略剩余值
..
模式会忽略模式中剩余的任何没有显式匹配的值部分,必须是无歧义的
19.3.13 匹配守卫提供的额外条件
- 匹配守卫(match guard)是一个指定于
match
分支模式之后的额外 if 条件,它也必须被满足才能选择此分支;匹配守卫用于表达比单独的模式所能允许的更为复杂的情况(需要同时满足更多条件) - 匹配守卫可以解决模式中变量覆盖的问题!
19.3.14 @ 绑定
@
运算符:允许在创建一个存放值的变量的同时测试其值是否匹配模式,也即先进行匹配测试,若通过则保存变量值到一个新建的变量中
19.4 小结
20. Rust 的高级特征
20.1 不安全 Rust
20.1.1 不安全的超能力
20.1.2 解引用裸指针
- 裸指针(raw pointers):类似于引用类型;和引用一样,裸指针是不可变或可变的,分别写作
*const T
和*mut T
,这里的星号不是解引用运算符,它是类型名称的一部分 - 在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值
- 直接从保证安全的引用来创建他们(比如使用
as
来强转为某类型),可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设! - 裸指针与引用和智能指针的区别,如下 4 点
- 允许忽略借用规则:可以同时拥有不可变和可变的指针(若通过可变指针修改数据,则可能潜在造成数据竞争!),或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
20.1.3 调用不安全函数或方法(一):基本概念
20.1.4 调用不安全函数或方法(二):创建不安全代码的安全抽象
slice::from_raw_parts_mut
函数是不安全的因为它获取一个裸指针,并必须确信这个指针是有效的- 裸指针上的
add
方法也是不安全的,因为其必须确信此地址偏移量也是有效的指针 - 注意无需将
split_at_mut
函数的结果标记为unsafe
,并可以在安全 Rust 中调用此函数。我们创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了unsafe
代码
20.1.5 调用不安全函数或方法(三):使用 extern 函数调用外部代码
20.1.6 访问或修改可变静态变量
- 常量:
- 不可变静态变量:静态变量中的值有一个固定的内存地址(使用这个值总是会访问相同的地址);访问不可变静态变量是安全的
- 可变静态变量:使用
mut
关键字来指定可变性;访问和修改可变静态变量都是 不安全 的,必须位于unsafe
模块内
20.1.7 实现不安全 trait
20.1.8 访问联合体中的字段
20.1.9 何时使用不安全代码
20.2 高级 trait
20.2.1 关联类型:在 trait 定义中指定占位符类型
- **关联类型(associated types)**是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型
- 关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符
- 作用:用于类型占位,不用重复多次仅修改类型来实现同一个功能
20.2.2 默认泛型类型参数和运算符重载
- 默认参数类型主要应用:1、扩展类型而不破坏现有代码;2、在大部分用户都不需要的特定情况进行自定义
20.2.3 完全限定语法与消歧义:调用相同名称的方法
- 同名问题:Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait,甚至直接在类型上实现开始已经有的同名方法也是可能的
- 对于方法(有
self
参数):编译器默认调用直接实现在类型上的方法 - 对于关联函数(没有
self
参数):直接调用定义于实现在类型中的关联函数 - 完全限定语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
,唯一地指定出哪个对象类型(type)所实现的哪个 trait 方法
20.2.4 父 trait 用于在另一个 trait 中使用某 trait 的功能
- 父(超)trait(supertrait):在实现 trait 时通过
子模块: 父模块
指定其依赖的上一级父 trait,即可获得父 trait 的方法。比如:OutlinePrint: fmt::Display
- 注意:需要同时满足子、父 trait 的实现要求!
20.2.5 newtype 模式用以在外部类型上实现外部 trait
20.3 高级类型
20.3.1 为了类型安全和抽象而使用 newtype 模式
20.3.2 类型别名用来创建类型同义词
- 类型别名(type alias):使用
type
关键字来给予现有类型另一个名字,主要用途是减少重复
20.3.3 从不返回的 never type
!
空类型 / 不返回类型:在函数从不返回的时候充当返回值,可以强转为任何其他类型
20.3.4 动态大小类型和 Sized trait
- Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小
- 动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后
Sized
trait 来决定一个类型的大小是否在编译时可知;这个 trait 自动为编译器在编译时就知道大小的类型实现;另外,Rust 隐式的为每一个泛型函数增加了 Sized bound
20.4 高级函数与闭包
20.4.1 函数指针
- 函数指针(function pointer):通过函数指针允许使用函数作为另一个函数的参数;函数满足类型
fn
(小写的 f),不要与闭包 trait 的Fn
(大写)相混淆 - 不同于闭包,
fn
是一个类型而不是一个 trait,所以直接指定fn
作为参数而不是声明一个带有Fn
作为 trait bound 的泛型参数 - 函数指针实现了所有三个闭包 trait(
Fn
、FnMut
和FnOnce
),所以总是可以在调用期望闭包的函数时传递函数指针作为参数
20.4.2 返回闭包
20.5 宏
20.5.1 宏和函数的区别
- 从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)
- 宏与函数的区别,如下 3 点
- 宏能够接收不同数量的参数,但函数签名必须声明函数参数个数和类型
- 在一个文件里调用宏 之前 必须定义它,或将其引入作用域;而函数则可以在任何地方定义和调用
- 宏定义通常要比函数定义更难阅读、理解以及维护
20.5.2 macro_rules! :声明宏用于通用元编程
20.5.3 用于从属性生成代码的过程宏
20.5.4 如何编写自定义 derive 宏
20.5.5 类属性宏
- 自定义派生宏:
derive
属性生成代码,derive
只能用于结构体和枚举 - 类属性宏:类属性宏与自定义派生宏相似,能创建新的属性,属性还可以用于其它的项(比如:函数),也更为灵活
20.5.6 类函数宏
- 类函数(Function-like)宏:定义看起来像函数调用的宏,类似于
macro_rules!
,它们比函数更灵活(例如,可以接受未知数量的参数)