写在前面
Rust 学习曲线陡峭,每次都去查Rust 程序设计语言,有点麻烦,集中记录一些常用的点。
Cargo 命令
1 | 新建项目 |
重要特点
语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。
函数调用是一个表达式。宏调用是一个表达式。用大括号创建的一个新的块作用域也是一个表达式。
函数的返回值等同于函数体最后一个表达式的值。
因为 if
是一个表达式,且Rust是静态强类型,要求编译时知道所有的变量类型,所以不同的分支返回值类型要相同。
loop 也是表达式。break ... ;
可以返回值
loop label 语法: ‘xxx : loop{}
所有权规则
首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量离开作用域后,Rust自动调用Drop
函数
如果类型都是已知大小的,可以存储在栈中,并且当离开作用域时被移出栈。如果需要跨作用域使用,则最好将数据存放在堆中。
变量与数据的交互规则:移动 move
浅拷贝,将所有权移交给新变量。将值传递给函数在语义上与给变量赋值相似
变量与数据的交互规则:克隆 clone
深拷贝
只在栈上的数据:拷贝
如果一个类型实现了 Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
返回值与作用域
返回值移动给调用它的函数
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop
被清理掉,除非数据被移动为另一个变量所有。
如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。
引用与借用
引用(reference)像一个指针,因为它是一个地址,但是与指针不同,引用确保指向某个特定类型的有效值。
&引用
*解引用
我们将创建一个引用的行为称为 借用(borrowing)。
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
允许我们修改一个借用的值,这就是 可变引用
可变引用注意点:
不可以同时拥有一个可变引用和一个不可变引用
多个不可变引用是可以的
在特定的作用域内,对于某一块数据只能有一个可变的引用
可以通过创建新的作用域,允许非同时地创建多个可变引用
1 | let mut s = String::from("hello"); |
编译器在作用域结束之前判断不再使用的引用的能力非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)
悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态。当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
引用规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
切片
字符串字面值就是 slice
1 | fn first_word(s: &str) -> &str { |
如果有一个 String
,则可以传递整个 String
的 slice 或对 String
的引用。这种灵活性利用了 deref coercions 的优势
结构体
struct的整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。
参数名与字段名都完全相同,我们可以使用 字段初始化简写语法
1 | fn build_user(email: String, username: String) -> User { |
struct 更新语法
..user1 必须放在最后,以指定其余的字段应从 user1
的相应字段中获取其值
1 | fn main() { |
元组结构体(tuple structs)
当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时使用
类单元结构体(unit-like structs)
类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。??
Struct 数据的所有权
生命周期确保结构体引用的数据有效性跟结构体本身保持一致。
如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的
整个结构体是有效的话其数据也是有效的
std::fmt::Display
std::fmt::Debug
:?
指示符告诉 println!
我们想要使用叫做 Debug
的输出格式。Debug
是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体
在结构体定义之前加上外部属性 #[derive(Debug)]
{:#?}
风格
方法
它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
它们在结构体的上下文中被定义或者是枚举或 trait 对象的上下文
它们第一个参数总是 self
它代表调用该方法的结构体实例。
&self
实际上是 self: &Self
的缩写。Self
类型是 impl
块的类型的别名
方法可以选择获得 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方
Rust 会自动为 object
添加 &
、&mut
或 *
以便使 object
与方法签名匹配。
这种自动引用的行为之所以有效,,是因为方法有一个明确的接收者———— self
的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。
关联函数
不以 self
为第一参数的关联函数(因此不是方法)
:: 关联函数, 也可用于模块创建的命名空间
枚举
枚举允许你通过列举可能的 成员(variants) 来定义一个类型。
Rust 的枚举与 F#、OCaml 和 Haskell 这样的函数式编程语言中的 代数数据类型(algebraic data types)最为相似。
可以将数据附加到枚举的变体中。
1 | enum IpAddr { |
1 |
|
Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude 之中。
可以不需要 Option::
前缀来直接使用 Some
和 None
用于描述 一个值要么有值要么没值,没有NULL(空值)
match 控制流结构
将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。
模式可由字面值、变量、通配符和许多其他内容构成;
match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
match xx {} 是一个表达式
绑定值的匹配
可以用于匹配option
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
if let简洁控制流
处理只匹配一个模式的值而忽略其他模式的情况,语法糖
包 crate和模块
Clear explanation of Rust’s module system
Rust模块系统-当main.rs和lib.rs同时存在的坑
Rust 中,crate
是一个独立的可编译单元。
具体说来,就是一个或一批文件(如果是一批文件,那么有一个文件是这个 crate 的入口)。它编译后,会对应着生成一个可执行文件或一个库。
包 中至少包含一个crate, 至多一个library crate,任意多个 二进制 crate
crate 默认的入口文件: src/main.rs 或者 src/lib.rs
通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
Rust 中所有条目(函数,方法,struct,enum,模块,常量)默认私有。
私有模块对同一模块内和下级模块可用。
父级模块无法访问子模块中的私有条目
子模块可以使用所有祖先模块中的条目
如果模块x与main方法在一个.rs文件中,且x处于最外层,main方法可以调用x中的方法。(根级)
子模块可以调用父模块中的private函数,但是反过来是不行的.(老爸的钱,就是儿子的钱,但是儿子的钱,除非儿子主动给老爸,否则还是儿子的!)
如果我们在一个结构体定义的前面使用了 pub
,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的
- use即可以在函数体内,也可以在函数外
- 当2个模块的函数有重名时,可以用use .. as .. 来取个别名
module 拆分到多个文件中:
与常规mod不同的是,mod x后,并没有{…}代码块,而是;号,rust会在同级目录下,默认去找x.rs,再来看main方法:
使用 mod <路径>
语法,将一个 rust 源码文件作为模块内引入:
1 | mod a; |
pub struct
结构体会变成公有的,但是这个结构体的字段仍然是私有的
back_of_house::Breakfast
具有私有字段,所以这个结构体需要提供一个公共的关联函数来构造 Breakfast
的实例
pub enum
如果我们将枚举设为公有,则它的所有成员都将变为公有。
use
函数引入到父级模块
其他直接引入到本身
同名条目,指定到父级
as
使用 as
关键字提供新的名称
pub use
重导出(re-exporting)
使用 use
关键字,将某个名称导入当前作用域后,这个名称在此作用域中就可以使用了,但它对此作用域之外还是私有的。
1 | pub use crate::front_of_house::hosting; |
我们不仅将一个名称导入了当前作用域,还允许别人把它导入他们自己的作用域。
使用外部包
- 添加依赖和版本
- use 将特定条目引入作用域
P31 3:00 之后
1 | // --snip-- |
拆分模块为多个文件
在 mod front_of_house
后使用分号,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。
创建一个 src/front_of_house 目录和一个包含 hosting
模块定义的 src/front_of_house/hosting.rs
常用集合
vector
vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
不能同时存在可变的借用和不可变的借用
vector + enum 可以存放多种类型的数据,提前知晓多种数据的长度,便于堆内存分配。
对于不确定的类型,可以使用trait对象
String
+拼接字符串,类似
1 | add(self,s:&str) ->String() |
解引用强制转换 &String -> &str
HashMap
哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。这些引用指向的值必须至少在哈希 map 有效时也是有效的。
or_insert
方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
错误处理
set RUST_BACKTRACE
= 1环境变量来得到一个 backtrace
为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release
参数运行 cargo build
1 | enum Result<T, E> { |
T
和 E
是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 T
代表成功时返回的 Ok
成员中的数据的类型,而 E
代表失败时返回的 Err
成员中的错误的类型。
失败时 panic 的简写:unwrap
和 expect
错误传播
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。
结尾的 ?
将会把 Ok
中的值返回给变量 f
。如果出现了错误,?
运算符会提早返回整个函数并将一些 Err
值传播给调用者。
函数的返回值必须是 Result
才能与这个 return
相兼容。
验证逻辑写进构造函数
泛型 trait 和 生命周期
泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联
trait,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。
Rust 类型名的命名规范是骆驼命名法(CamelCase)
泛型
你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你的代码中需要许多泛型类型时,它可能表明你的代码需要重构,分解成更小的结构。
可以在函数,结构体,枚举,方法中使用泛型
注意必须在 impl
后面声明 T
,这样就可以在 Point<T>
上实现的方法中使用它了
定义方法适用于某些有限制(constraint)的泛型类型。例如,可以选择为 Point<f32>
实例实现方法,而不是为泛型 Point
实例。
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。
tarit
trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。
可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。
只有当至少一个 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。
不能为外部类型实现外部 trait
这个限制是被称为 相干性(coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码
默认实现
无法从相同方法的重载实现中调用默认方法
trait 作为参数
该参数是实现了 Summary
trait 的某种类型。
返回实现了 trait 的类型
这里尝试返回 NewsArticle
或 Tweet
。这不能编译,因为 impl Trait
工作方式的限制。
函数返回值类型不能 不同
使用 trait bound 有条件地实现方法
1 | use std::fmt::Display; |
只有实现了 Display + PartialOrd 这两个trait 的 类型 T 才具有 cmp_display 方法
1 | impl<T: Display> ToString for T { // --snip-- |
为所有实现了 Display 这个 trait 的 类型 T 实现 ToString 这个trait
生命周期
生命周期则有助于确保引用在我们需要他们的时候一直有效。
避免悬垂引用
记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {// --snip-- |
被 'a
所替代的具体生命周期是 x
的作用域与 y
的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a
的具体生命周期等同于 x
和 y
的生命周期中较小的那一个。
生命周期省略规则:
- 第一条规则是每一个是引用的参数都有它自己的生命周期参数。
- 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
- 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是
&self
或&mut self
,说明是个对象的方法(method), 那么所有输出生命周期参数被赋予self
的生命周期。
方法定义中的生命周期注解
'static
,其生命周期能够存活于整个程序期间
自动化测试
#[test] 属性添加在测试函数前,不发生panic 通过
use super::* 导入测试的模块
assert!(bool, msg, …)
assert_eq!(a,b,msg,…)
assert_ne!(a,b,msg,…) msg可以使用{}
#[should_panic] 发生panic 通过
针对Result<T,E>
测试. 不能对这些使用 Result<T, E>
的测试使用 #[should_panic]
注解。
不要使用对 Result<T, E>
值使用问号表达式(?
),而是使用 assert!(value.is_err())
Rust 默认使用线程来并行运行。
1 | cargo test -- --test-threads=1 |
1 | cargo test -- --show-output |
--show-output
告诉 Rust 显示成功测试的输出。
可以向 cargo test
传递任意测试的名称来只运行这个测试。
我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。
#[ignore] 忽略测试函数
当你需要运行 ignored
的测试时,可以执行 cargo test -- --ignored
。如果你希望不管是否忽略都要运行全部测试,可以运行 cargo test -- --include-ignored
。
单元测试
单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的某个单元的代码功能是否符合预期。
单元测试与他们要测试的代码共同存放在位于 src 目录下相同的文件中。规范是在每个文件中创建包含测试函数的 tests
模块,并使用 cfg(test)
标注模块, 注解告诉 Rust 只在执行 cargo test
时才编译和运行测试代码
Rust 的私有性规则确实允许你测试私有函数。
集成测试
需要在项目根目录创建一个 tests 目录,与 src 同级。
接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
文件名: tests/integration_test.rs
1 | use adder; // 被测试的lib crate |
可以通过指定测试函数的名称作为 cargo test
的参数来运行特定集成测试。也可以使用 cargo test
的 --test
后跟文件的名称来运行某个特定集成测试文件中的所有测试
1 | cargo test --test integration_test |
二进制 crate 的集成测试
只有库 crate 才会向其他 crate 暴露了可供调用和使用的函数;二进制 crate 只意在单独运行
Rust 二进制项目的结构明确采用 src/main.rs 调用 src/lib.rs 中的逻辑的方式就可以 通过 extern crate
测试库 crate 中的主要功能
项目CLI
对 mod
和 use
进行区分:**use
仅仅是在存在模块的前提下,调整调用路径,而没有引入模块的功能,引入模块使用 mod
**
函数式编程,迭代器与闭包
闭包
Rust 的 闭包(closures)是可以保存在一个变量中或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。
闭包不要求像 fn
函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解
1 | let expensive_closure = |num: u32| -> u32 { |
使用竖线而不是括号以及几个可选的语法之外:
1 | fn add_one_v1 (x: u32) -> u32 { x + 1 } |
如果尝试对同一闭包使用不同类型则会得到类型错误
memoization 或 lazy evaluation (惰性求值)
为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。
每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。
Fn
系列 trait 由标准库提供。所有的闭包都实现了 trait Fn
、FnMut
或 FnOnce
中的一个。
FnOnce
消费从周围作用域捕获的变量,闭包周围的作用域被称为其 环境,environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的Once
部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut
获取可变的借用值所以可以改变其环境Fn
从其环境获取不可变的借用值
move 关键字
x
被移动进了闭包,因为闭包使用 move
关键字定义。接着闭包获取了 x
的所有权,同时 main
就不再允许在 println!
语句中使用 x
了。
1 | fn main() { |
大部分需要指定一个 Fn
系列 trait bound 的时候,可以从 Fn
开始,而编译器会根据闭包体中的情况告诉你是否需要 FnMut
或 FnOnce
。
迭代器
迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。
迭代器都实现了一个叫做 Iterator
的定义于标准库的 trait
1 | pub trait Iterator { |
type Item
和 Self::Item
,他们定义了 trait 的 关联类型
next
是 Iterator
实现者被要求定义的唯一方法。next
一次返回迭代器中的一个项,封装在 Some
中,当迭代器结束时,它返回 None
。
在迭代器上调用 next
方法改变了迭代器中用来记录序列位置的状态。代码 消费(consume)了,或使用了迭代器。
for
循环时无需使 v1_iter
可变因为 for
循环会获取 v1_iter
的所有权并在后台使 v1_iter
可变。
调用 next
方法的方法被称为 消费适配器(consuming adaptors),因为调用他们会消耗迭代器
iter
方法生成一个不可变引用的迭代器。
如果我们需要一个获取 v1
所有权并返回拥有所有权的迭代器,则可以调用 into_iter
。
如果我们希望迭代可变引用,则可以调用 iter_mut
。
产生其他迭代器的方法
Iterator
trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。
不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
map
方法使用闭包来调用每个元素以生成新的迭代器。
1 | let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); |
filter
方法获取一个使用迭代器的每一个项并返回布尔值的闭包。如果闭包返回 true
,其值将会包含在 filter
提供的新迭代器中。如果闭包返回 false
,其值不会包含在结果迭代器中。
1 | fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { |
闭包从环境中捕获了 shoe_size
变量并使用其值与每一只鞋的大小作比较,只保留指定大小的鞋子。最终,调用 collect
将迭代器适配器返回的值收集进一个 vector 并返回。
实现 Iterator
trait 来创建自定义迭代器
1 | fn using_other_iterator_trait_methods() { |
zip 将两个迭代器,返回新的迭代器,新的迭代器返回一个元组,其中的值为输入的两个迭代器产生的序列中对应的值
cargo 与 crate.is
发布配置(release profiles)是预定义的、可定制的带有不同选项的配置。每一个配置都彼此相互独立。
Cargo 有两个主要的配置:dev
配置、release
配置
通过增加任何希望定制的配置对应的 [profile.*]
部分,我们可以选择覆盖任意默认设置的子集。
1 | [profile.dev] |
文档注释使用三斜杠 ///
而不是两斜杆以支持 Markdown 注解来格式化文本。
# Examples
Markdown 标题在 HTML 中创建了一个以 “Examples” 为标题的部分。其他一些 crate 作者经常在文档注释中使用的部分有:
- Panics:这个函数可能会
panic!
的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。 - Errors:如果这个函数返回
Result
,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。 - Safety:如果这个函数使用
unsafe
代码(这会在第十九章讨论),这一部分应该会涉及到期望函数调用者支持的确保unsafe
块中代码正常工作的不变条件(invariants)。
Rust 也有特定的用于文档的注释类型,通常被称为 文档注释
文档注释,//!
,这为包含注释的项,而不是位于注释之后的项增加文档。
pub use
你可以选择使用 pub use
重导出(re-export)项来使公有结构不同于私有结构。
1 | //! # Art |
1 | use art::mix; |
缩短使用者的路径
发布到 Crates.io
发布 crate 时请多加小心,因为发布是 永久性的(permanent)。对应版本不可能被覆盖,其代码也不可能被删除。crates.io 的一个主要目标是作为一个存储代码的永久文档服务器,这样所有依赖 crates.io 中的 crate 的项目都能一直正常工作。
license 和 description 字段 必需
1 | cargo publish |
虽然你不能删除之前版本的 crate,但是可以阻止任何将来的项目将他们加入到依赖中。这在某个版本因为这样或那样的原因被破坏的情况很有用。对于这种情况,Cargo 支持 撤回(yanking)某个版本。
1 | cargo yank --vers 1.0.1 |
工作空间
工作空间 是一系列共享同样的 Cargo.lock 和输出目录的包。
Cargo.toml 它以 [workspace]
部分作为开始,并通过指定 adder 的路径来为工作空间增加成员
1 | [workspace] |
通过共享一个 target 目录,工作空间可以避免其他 crate 多余的重复构建。
为了在顶层 add 目录运行二进制 crate,可以通过 -p
参数和包名称来运行 cargo run
指定工作空间中我们希望使用的包:
1 | cargo run -p adder |
工作空间只在根目录有一个 Cargo.lock
使得工作空间中的所有 crate 都使用相同的依赖意味着其中的 crate 都是相互兼容的。
每个crate 需要独立写各自的cargo.toml,以此确定依赖
工作空间中的测试,如果在顶级目录cargo test,则运行所有crate 的测试。可以 -p 指定具体的 crate
发布工作空间中的 crate,每一个工作空间中的 crate 需要单独发布。
从crate.io 安装二进制crate
cargo install
二进制目标 文件是在 crate 有 src/main.rs 或者其他指定为二进制文件时所创建的可执行程序
默认安装目录是 $HOME/.cargo/bin
,确保将这个目录添加到 $PATH
环境变量中就能够运行通过 cargo install
安装的程序了
自定义命令扩展cargo
如果 $PATH
中有类似 cargo-something
的二进制文件,就可以通过 cargo something
来像 Cargo 子命令一样运行它。
运行 cargo --list
来展示出来
智能指针
指针 (pointer)是一个包含内存地址的变量的通用概念。
这个地址引用,或 “指向”(points at)一些其他数据。
Rust 中最常见的指针是第四章介绍的 引用(reference)。引用以 &
符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。
智能指针(smart pointers)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。
引用是一类只借用数据的指针;在大部分情况下,智能指针 拥有 他们指向的数据。
智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 Deref
和 Drop
trait。
Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
使用Box
指向堆上的数据
最简单直接的智能指针是 box,其类型是 Box<T>
, box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。
当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
存放在栈上的指针是大小确定的
当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
同上,通过只有少量的指针数据在栈上被拷贝,避免大量的数据传输
当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
trait 对象(trait object)
Box 允许创建递归类型
代表递归的终止条件(base case)的规范名称是 Nil
,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。
cons list 是一个来源于 Lisp 编程语言及其方言的数据结构。
cons list 的每一项都包含两个元素:当前项的值和下一项。
其最后一项值包含一个叫做 Nil
的值且没有下一项。cons list 通过递归调用 cons
函数产生。
1 | enum List { |
通过 Deref trait 将智能指针当作常规引用处理
实现 Deref
trait 允许我们重载 解引用运算符
从根本上说,Box<T>
被定义为包含一个元素的元组结构体
1 | struct MyBox<T>(T); |
*y
,Rust 事实上在底层运行了如下代码:
1 | *(y.deref()) |
每次当我们在代码中使用 *
时, *
运算符都被替换成了先调用 deref
方法再接着使用 *
解引用的操作,且只会发生一次,不会对 *
操作符无限递归替换
函数和方法的隐式 Deref 强制转换
Deref 强制转换(deref coercions)是 Rust 在函数或方法传参上的一种便利
Deref 强制转换只能作用于实现了 Deref
trait 的类型。Deref 强制转换将这样一个类型的引用转换为另一个类型的引用。
当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,Deref 强制转换将自动发生。这时会有一系列的 deref
方法被调用,把我们提供的类型转换成了参数所需的类型。
这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗
1 | use std::ops::Deref; |
Deref
trait 重载不可变引用的 *
运算符
DerefMut
trait 用于重载可变引用的 *
运算符
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
- 当
T: Deref<Target=U>
时从&T
到&U
。 - 当
T: DerefMut<Target=U>
时从&mut T
到&mut U
。 - 当
T: Deref<Target=U>
时从&mut T
到&U
。
第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 不可能 的
因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。
使用 Drop Trait 运行清理代码
对于智能指针模式来说第二个重要的 trait 是 Drop
,其允许我们在值要离开作用域时执行一些代码。
Drop
trait 包含在 prelude 中
变量以被创建时相反的顺序被丢弃
通过 std::mem::drop
提早丢弃值
我们并不能直截了当的禁用 drop
这个功能。通常也不需要禁用 drop
;整个 Drop
trait 存在的意义在于其是自动处理的。
Rust 不允许我们显式调用 drop
因为 Rust 仍然会在 main
的结尾对值自动调用 drop
,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。
如果我们需要强制提早清理值,可以使用 std::mem::drop
函数。
std::mem::drop
函数不同于 Drop
trait 中的 drop
方法。可以通过传递希望提早强制丢弃的值作为参数。std::mem::drop
位于 prelude
Rc 引用计数智能指针
Rc<T>
用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。
如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效
Rc<T>
只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。
Rc::clone
只会增加引用计数。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 Rc::clone
调用
Rc::strong_count
查看引用计数
不必像调用 Rc::clone
增加引用计数那样调用一个函数来减少计数;Drop
trait 的实现当 Rc<T>
值离开作用域时自动减少引用计数。
通过不可变引用, Rc<T>
允许在程序的多个部分之间只读地共享数据
如果 Rc<T>
也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!
我们将讨论内部可变性模式和 RefCell<T>
类型,它可以与 Rc<T>
结合使用来处理不可变性的限制
RefCell
和内部可变性模式
内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。
该模式在数据结构中使用 unsafe
代码来模糊 Rust 通常的可变性和借用规则。
借用规则:
- 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 之一(而不是两者)。
- 引用必须总是有效的。
对于引用和 Box<T>
,借用规则的不可变性作用于编译时。对于 RefCell<T>
,这些不可变性作用于 运行时。
对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T>
,如果违反这些规则程序会 panic 并退出。
静态分析总是保守的,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。RefCell<T>
正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。
RefCell<T>
只能用于单线程场景。
在不可变值内部改变值就是 内部可变性 模式。
特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。
RefCell<T>
并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现 panic 而不是编译错误。
测试替身(test double)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。mock 对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
将Rc 和 RefCell结合来拥有多个可变数据所有者
回忆一下 Rc<T>
允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T>
的 Rc<T>
的话,就可以得到有多个所有者 并且 可以修改的值了!
Rc<RefCell<i32>>
实例并储存在变量 value
中以便之后直接访问。
CellCell<T>
Mutex<T>
,其提供线程间安全的内部可变性
rust - Rc<RefCell
循环引用导致内存泄露
避免引用循环:将 Rc
变为 Weak
Rc::downgrade
并传递 Rc<T>
实例的引用来创建其值的 弱引用(weak reference)
调用 Rc::downgrade
时会得到 Weak<T>
类型的智能指针。
调用 Rc::downgrade
会将 weak_count
加 1。
Rc<T>
类型使用 weak_count
来记录其存在多少个 Weak<T>
引用,类似于 strong_count
。其区别在于 weak_count
无需计数为 0 就能使 Rc<T>
实例被清理。
强引用代表如何共享 Rc<T>
实例的所有权,但弱引用并不属于所有权关系。
为了使用 Weak<T>
所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak<T>
实例的 upgrade
方法,这会返回 Option<Rc<T>>
。
如果 Rc<T>
值还未被丢弃,则结果是 Some
;如果 Rc<T>
已被丢弃,则结果是 None
。
例子:
建立树,父对子 是strong_count, 子对父则是 weak_count.
1 | struct Node { |
无畏并发
很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模型有时被称为 1:1
thread::spawn(|| { … })
当主线程结束时,新线程也会结束,而不管其是否执行完毕。
通过将 thread::spawn
的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn
的返回值类型是 JoinHandle
。JoinHandle
是一个拥有所有权的值,当对其调用 join
方法时,它会等待其线程结束。
线程与 move 闭包
move
关键字经常用于传递给 thread::spawn
的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。
使用消息传递在线程间传送数据
Rust 中一个实现消息传递并发的主要工具是 信道(channel)
当发送者或接收者任一被丢弃时可以认为信道被 关闭(closed)了
这里使用 mpsc::channel
函数创建一个新的信道;mpsc
是 多个生产者,单个消费者(multiple producer, single consumer)
信道的接收端有两个有用的方法:recv
和 try_recv
。
recv
,这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,recv
会在一个 Result<T, E>
中返回它。当信道发送端关闭,recv
会返回一个错误表明不会再有新的值到来了。
try_recv
不会阻塞,相反它立刻返回一个 Result<T, E>
:Ok
值包含可用的信息,而 Err
值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv
很有用:可以编写一个循环来频繁调用 try_recv
,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
信道与所有权转移
move 与 thread::spawn 结合,将 所有权转移至 闭包内部
1 | thread::spawn(move || { |
发送完成之后, val 的所有权转移给了接受者
通过克隆发送者来创建多个生产者
1 | let (tx, rx) = mpsc::channel(); |
共享状态并发
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。
- 在使用数据之前尝试获取锁。
- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
使用关联函数 new
来创建一个 Mutex<T>
如果另一个线程拥有锁,并且那个线程 panic 了,则 lock
调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 unwrap
并在遇到这种情况时使线程 panic。
Mutex<T>
是一个智能指针。更准确的说,lock
调用 返回 一个叫做 MutexGuard
的智能指针。这个智能指针实现了 Deref
来指向其内部数据;其也提供了一个 Drop
实现当 MutexGuard
离开作用域时自动释放锁
在线程间共享 Mutex
直接move和Rc
原子引用计数 Arc
使用 Sync
和 Send
trait 的可扩展并发
两个并发概念是内嵌于语言中的:std::marker
中的 Sync
和 Send
trait。
Send
标记 trait 表明实现了 Send
的类型值的所有权可以在线程间传送。
不过有一些例外,包括 Rc<T
,Rc<T>
被实现为用于单线程场景
任何完全由 Send
的类型组成的类型也会自动被标记为 Send
。
几乎所有基本类型都是 Send
的,除了第十九章将会讨论的裸指针(raw pointer)。
Sync
标记 trait 表明一个实现了 Sync
的类型可以安全的在多个线程中拥有其值的引用。
对于任意类型 T
,如果 &T
(T
的不可变引用)是 Send
的话 T
就是 Sync
的,这意味着其引用就可以安全的发送到另一个线程。
类似于 Send
的情况,基本类型是 Sync
的,完全由 Sync
的类型组成的类型也是 Sync
的。
智能指针 Rc<T>
也不是 Sync
的
RefCell<T>
和 Cell<T>
系列类型不是 Sync
的。
RefCell<T>
在运行时所进行的借用检查也不是线程安全的。
Mutex<T>
是 Sync
的,它可以被用来在多线程中共享访问。
Rust 面向对象 编程特性
对象包含数据和行为
面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法 或 操作。
封装隐藏了实现细节
注意,结构体自身被标记为 pub
,这样其他代码就可以使用这个结构体,但是在结构体内部的字段仍然是私有的。
继承,作为类型系统与代码共享
第一个是为了重用代码
Rust 代码可以使用默认 trait 方法实现来进行共享
第二个使用继承的原因与类型系统有关:表现为子类型可以用于父类型被使用的地方。这也被称为 多态(polymorphism),这意味着如果多种对象共享特定的属性,则可以相互替代使用。
1 | pub trait Summary { |
近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。
Rust 则通过泛型来对不同的可能类型进行抽象,并通过 trait bounds 对这些类型所必须提供的内容施加约束。
trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的trait方法的表。
我们通过指定某种指针来创建 trait 对象,例如 &
引用或 Box<T>
智能指针,还有 dyn
keyword, 以及指定相关的 trait
我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。
这个 vector 的类型是 Box<dyn Draw>
,此为一个 trait 对象:它是 Box
中任何实现了 Draw
trait 的类型的替身。
如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。
通过使用 trait 对象的方法,一个 Screen
实例可以存放一个既能包含 Box<Button>
,也能包含 Box<TextField>
的 Vec<T>
单态化所产生的代码进行 静态分发(static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。
这与 动态分发 (dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。
当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。
动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
只有对象安全(object-safe)的trait可以实现为特征对象。
如果一个 trait 中定义的所有方法都符合以下规则,则该 trait 是对象安全的:
- 返回值不是
Self
- 没有泛型类型的参数
Self
关键字是我们在 trait 与方法上的实现的别称,trait 对象必须是对象安全的,因为一旦使用 trait 对象,Rust 将不再知晓该实现的返回类型。如果一个 trait 的方法返回了一个 Self
类型,但是该 trait 对象忘记了 Self
的确切类型,那么该方法将不能使用原本的类型。
当 trait 使用具体类型填充的泛型类型时也一样:具体类型成为实现 trait 的对象的一部分,当使用 trait 对象却忘了类型是什么时,无法知道应该用什么类型来填充泛型类型。
一个非对象安全的 trait 例子是标准库中的 Clone
trait。Clone
trait 中的 clone
方法的声明如下:
1 |
|
面向对象设计模式的实现
状态模式(state pattern)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 状态对象,同时值的行为随着其内部状态而改变。
每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
模式与模式匹配
模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。
模式由如下一些内容组合而成:
- 字面值
- 解构的数组、枚举、结构体或者元组
- 变量
- 通配符
- 占位符
match 分支
if let 条件表达式
可以组合并匹配 if let
、else if
和 else if let
表达式。
while let 条件循环
1 | let mut stack = Vec::new(); |
for 循环
1 | fn main() { |
let 语句
1 | let PATTERN = EXPRESSION; |
let x = 5;
这样的语句中变量名位于 PATTERN
位置,变量名不过是形式特别朴素的模式。
我们将表达式与模式比较,并为任何找到的名称赋值。所以例如 let x = 5;
的情况,x
是一个代表 “将匹配到的值绑定到变量 x” 的模式。同时因为名称 x
是整个模式,这个模式实际上等于 “将任何值绑定到变量 x
,不管值是什么”。
函数参数
1 | fn foo(x: i32) { |
x
部分就是一个模式!类似于之前对 let
所做的,可以在函数参数中匹配元组。
1 | fn print_coordinates(&(x, y): &(i32, i32)) { |
因为如第十三章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。
Refutability(可反驳性): 模式是否会匹配失效
模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。
能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)。
一个例子就是 let x = 5;
语句中的 x
,因为 x
可以匹配任何值所以不可能会失败。
对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable)
。一个这样的例子便是 if let Some(x) = a_value
表达式中的 Some(x)
;如果变量 a_value
中的值是 None
而不是 Some
,那么 Some(x)
模式不能匹配。
函数参数、 let
语句和 for
循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。
if let
和 while let
表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。
Rust 会抱怨将不可反驳模式用于 if let
是没有意义的:
基于此,match
匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。Rust允许我们在只有一个匹配分支的match
中使用不可反驳模式,可以用if let
。
所有的模式语法
匹配字面值
匹配命名变量
用于 match
表达式时,因为 match
会开始一个新作用域,match
表达式中作为模式的一部分声明的变量会覆盖 match
结构之外的同名变量。
多个模式
在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)的意思。
1 | fn main() { |
通过 ..= 匹配值的范围
..=
语法允许你匹配一个闭区间范围内的值。
除了数字字面值之外,还可以匹配char
1 | fn main() { |
解构并分解值
使用模式来解构结构体、枚举和元组,以便使用这些值的不同部分。
解构结构体
1 | struct Point { |
因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p;
包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。
1 | let p = Point { x: 0, y: 7 }; |
使用字面值作为结构体模式的一部分进行解构,而不是为所有的字段创建变量
1 | let p = Point { x: 0, y: 7 }; |
第一个分支通过指定字段 y
匹配字面值 0
来匹配任何位于 x
轴上的点,此模式仍然创建了变量 x
以便在分支的代码中使用。
第二个分支通过指定字段 x
匹配字面值 0
来匹配任何位于 y
轴上的点,并为字段 y
创建了变量 y
。
解构枚举
解构枚举的模式需要对应枚举所定义的储存数据的方式。
1 | enum Message { |
对于像 Message::Quit
这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit
,因此模式中没有任何变量。
对于像 Message::Move
这样的类结构体枚举成员,可以采用类似于匹配结构体的模式。
对于像 Message::Write
这样的包含一个元素,以及像 Message::ChangeColor
这样包含三个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。
解构嵌套的结构体和枚举
当然也可以匹配嵌套的项!
1 | enum Color { |
解构结构体和元组
1 | let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); |
忽略模式中的值
使用过下划线(_
)作为匹配但不绑定任何值的通配符模式了
虽然 _
模式作为 match
表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中
1 | fn foo(_: i32, y: i32) { |
大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。
使用嵌套的 _ 忽略部分值
1 | let mut setting_value = Some(5); |
也可以在一个模式中的多处使用下划线来忽略特定值
1 | match numbers { |
通过在名字前以一个下划线开头来忽略未使用的变量
只使用 _
和使用以下划线开头的名称有些微妙的不同:比如 _x
仍会将值绑定到变量,而 _
则完全不会绑定。
1 | let s = Some(String::from("Hello!")); |
用 .. 忽略剩余值
对于有多个部分的值,可以使用 ..
语法来只使用部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。
1 | struct Point { |
不得不列出 y: _
和 z: _
要来得简单
1 | let numbers = (2, 4, 8, 16, 32); |
然而使用 ..
必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。
比如,不能(.., second, ..)
匹配守卫提供的额外条件
匹配守卫(match guard)是一个指定于 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。
1 | let num = Some(4); |
这种额外表现力的缺点在于当涉及匹配守卫表达式时编译器不会尝试检查穷尽性。
可以使用匹配守卫来解决模式中变量覆盖的问题
可以在匹配守卫中使用 或 运算符 |
来指定多个模式
1 | let x = 4; |
这个匹配条件表明此分支值匹配 x
值为 4
、5
或 6
同时 y
为 true
的情况。
匹配守卫与模式的优先级关系看起来像这样:
1 | (4 | 5 | 6) if y => ... |
@ 绑定
at 运算符(@
)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。
1 | enum Message { |
通过在 3..=7
之前指定 id_variable @
,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。
使用 @
可以在一个模式中同时测试和保存变量值
// 没有理解用途
高级特征
- 不安全 Rust:用于当需要舍弃 Rust 的某些保证并负责手动维持这些保证
- 高级 trait:与 trait 相关的关联类型,默认类型参数,完全限定语法(fully qualified syntax),超(父)trait(supertraits)和 newtype 模式
- 高级类型:关于 newtype 模式的更多内容,类型别名,never 类型和动态大小类型
- 高级函数和闭包:函数指针和返回闭包
- 宏:定义在编译时定义更多代码的方式
不安全 Rust
原因:
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。这必然意味着有时代码 可能 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。
底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。
不安全的超能力。” 这些超能力是:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union
的字段
解引用裸指针
裸指针与引用和智能指针的区别在于
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
裸指针是不可变或可变的,分别写作 *const T
和 *mut T
。这里的星号不是解引用运算符;它是类型名称的一部分。
创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。
解引用裸指针必须在 unsafe块内
调用不安全的函数或方法
在此上下文中,关键字unsafe
表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe
块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。
1 | fn main() { |
不安全函数体也是有效的 unsafe
块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe
块。
事实上,将不安全代码封装进安全函数是一个常见的抽象。
使用 extern 函数调用外部代码,"C"
部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI)
1 | extern "C" { |
从其它语言调用 Rust 函数,需要添加#[no_mangle]
注解,来告诉 Rust 编译器不要 mangle 此函数的名称。
1 |
|
访问或修改可变静态变量
全局变量在 Rust 中被称为 静态(static)变量。
通常静态变量的名称采用 SCREAMING_SNAKE_CASE
写法。静态变量只能储存拥有 'static
生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量
中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量
则允许在任何被用到的时候复制其数据。
访问和修改可变静态变量都是 不安全 的。
优先使用并发技术和线程安全智能指针
实现不安全 trait
当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait
之前增加 unsafe
关键字将 trait 声明为 unsafe
,同时 trait 的实现也必须标记为 unsafe
1 | unsafe trait Foo { |
编译器会自动为完全由 Send
和 Sync
类型组成的类型自动实现他们。如果实现了一个包含一些不是 Send
或 Sync
的类型,比如裸指针,并希望将此类型标记为 Send
或 Sync
,则必须使用 unsafe
。
Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe
表明
访问联合体中的字段
union
和 struct
类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。
访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。
高级 trait
关联类型在 trait 定义中指定占位符类型
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。
1 | pub trait Iterator { |
默认泛型类型参数和运算符重载
如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>
。
1 | trait Add<Rhs=Self> { |
比较陌生的部分是尖括号中的 Rhs=Self
:这个语法叫做 默认类型参数(default type parameters)。Rhs
是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add
方法中的 rhs
参数。如果实现 Add
trait 时不指定 Rhs
的具体类型,Rhs
的类型将是默认的 Self
类型,也就是在其上实现 Add
的类型。
我们指定 impl Add<Meters>
来设定 Rhs
类型参数的值而不是使用默认的 Self
。
1 | use std::ops::Add; |
完全限定语法与消歧义:调用相同名称的方法
1 | fn main() { |
1 | <Type as Trait>::function(receiver_if_method, next_arg, ...); |
父 trait 用于在另一个 trait 中使用某 trait 的功能
有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 父(超) trait(supertrait)。
1 | use std::fmt; |
newtype 模式用以在外部类型上实现外部 trait
孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。
一个绕开这个限制的方法是使用 newtype 模式(newtype pattern),它涉及到在一个元组结构体中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。
1 | use std::fmt; |
此方法的缺点是,因为 Wrapper
是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper
上实现 Vec<T>
的所有方法,这样就可以代理到self.0
上
如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。
高级类型
包括静态的确保某值不被混淆,和用来表示一个值的单位,。
Millimeters
和 Meters
结构体都在 newtype 中封装了 u32
值。如果编写了一个有 Millimeters
类型参数的函数,不小心使用 Meters
或普通的 u32
值来调用该函数的程序是不能编译的。
抽象掉一些类型的实现细节
例如,可以提供一个封装了 HashMap<i32, String>
的 People
类型,用来储存人名以及相应的 ID。使用 People
的代码只需与提供的公有 API 交互即可,比如向 People
集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 i32
ID 赋予了这个名字了。
类型别名用来创建类型同义词
Rust 还提供了声明 类型别名(type alias)的能力,使用 type
关键字来给予现有类型另一个名字。例如,可以像这样创建 i32
的别名 Kilometers
:
1 | type Kilometers = i32; |
类型别名通过减少项目中重复代码的数量来使其更加易于控制。
类型别名也经常与 Result<T, E>
结合使用来减少重复。
1 | type Result<T> = std::result::Result<T, std::io::Error>; |
从不返回的 never type
Rust 有一个叫做 !
的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如:
1 | fn bar() -> ! { |
而从不返回的函数被称为 发散函数(diverging functions)
正如你可能猜到的,continue
的值是 !
1 | let guess: u32 = match guess.trim().parse() { |
never type 的另一个用途是 panic!
1 | impl<T> Option<T> { |
最后一个有着 !
类型的表达式是 loop
:
1 | print!("forever "); |
动态大小类型和 Sized trait
因为 Rust 需要知道例如应该为特定类型的值分配多少空间这样的信息
这就是 动态大小类型(dynamically sized types)的概念。这有时被称为 “DST” 或 “unsized types”
str
是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 str
类型的变量,也不能获取 str
类型的参数。
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。
&str
则是 两个 值:str
的地址和其长度。这样,&str
就有了一个在编译时可以知道的大小:它是 usize
长度的两倍。
们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。
我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 &dyn Trait
或 Box<dyn Trait>
为了处理 DST,Rust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:这就是 Sized
trait。
另外,Rust 隐式的为每一个泛型函数增加了 Sized
bound。
1 | fn generic<T>(t: T) { |
实际上被当作如下处理:
1 | fn generic<T: Sized>(t: T) { |
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:
1 | fn generic<T: ?Sized>(t: &T) { |
?Sized
上的 trait bound 意味着 “T
可能是也可能不是 Sized
” 。这种意义的 ?Trait
语法只能用于 Sized
,而不能用于任何其他 trait。
另外注意我们将 t
参数的类型从 T
变为了 &T
:因为其类型可能不是 Sized
的,所以需要将其置于某种指针之后。在这个例子中选择了引用。
高级函数与闭包
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!
通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 fn
fn
被称为 函数指针(function pointer)。指定参数为函数指针的语法类似于闭包
1 | fn add_one(x: i32) -> i32 { |
不同于闭包,fn
是一个类型而不是一个 trait,所以直接指定 fn
作为参数而不是声明一个带有 Fn
作为 trait bound 的泛型参数。
函数指针实现了所有三个闭包 trait(Fn
、FnMut
和 FnOnce
),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。
一个只期望接受 fn
而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 map
的应用。使用 map
函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样:
1 | let list_of_numbers = vec![1, 2, 3]; |
或者可以将函数作为 map
的参数来代替闭包,像是这样:
1 | let list_of_numbers = vec![1, 2, 3]; |
注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 to_string
的函数;这里使用了定义于 ToString
trait 的 to_string
函数
另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节。这些项使用 ()
作为初始化语法,这看起来就像函数调用,同时它们确实被实现为返回由参数构造的实例的函数。
1 | enum Status { |
返回闭包
闭包表现为 trait,这意味着不能直接返回闭包。
错误又一次指向了 Sized
trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:
1 | fn returns_closure() -> Box<dyn Fn(i32) -> i32> { |
宏
宏(Macro)指的是 Rust 中一系列的功能:
使用 macro_rules!
的 声明(Declarative)宏,和三种 过程(Procedural)宏:
- 自定义
#[derive]
宏在结构体和枚举上指定通过derive
属性添加的代码 - 类属性(Attribute-like)宏定义可用于任意项的自定义属性
- 类函数宏看起来像函数不过作用于作为参数传递的 token
宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数
宏可以在编译器翻译代码前展开
实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
使用 macro_rules! 的声明宏用于通用元编程
1 |
|
#[macro_export]
注解表明只要导入了定义这个宏的crate,该宏就应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
接着使用 macro_rules!
和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec
用于从属性生成代码的过程宏
它们更像函数(一种过程类型)
过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。
1 | use proc_macro; |
其中 some_attribute
是一个使用特定宏的占位符。
Rust默认携带了proc_macro
crate。 这就是宏的核心:宏所处理的源代码组成了输入 TokenStream
,宏生成的代码是输出 TokenStream
。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
类属性宏
不同的是 derive
属性生成代码,它们(类属性宏)能让你创建新的属性。
可以创建一个名为 route
的属性用于注解 web 应用程序框架(web application framework)的函数:
1 |
|
类函数宏
类函数宏获取 TokenStream
参数,其定义使用 Rust 代码操纵 TokenStream
,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 sql!
宏:
1 | let sql = sql!(SELECT * FROM posts WHERE id=1); |