Skip to content

AHABHGK

Rust for Rustaceans

Foundations

Talking About Memory

Variables in Depth

在高层视角中,变量不是字节的表示,而是给值起的名字,可以看作一个有名字的点,值在初始化时被命名,作为起点,在被访问时将上一个同名的点连接,在 move、drop 时这个点消失,形成一个 “flow“,进而对 flow 进行 borrow check、drop check

Understanding Rust Lifetimes: a visual introduction - Presentation to the Kerala Rustacaeans

在底层视角中,变量是一个 “value slot”,赋值时丢弃旧值,访问时编译器检查是否为空,类似 C 的变量,这个视角主要用于明确推理内存

Memory Regions

Stack is a sagment of memory,当函数调用时会在 Stack 上分配一个连续的内存块,成为 “frame”,函数调用结束时这个 frame 中的内存全部会被回收,因此返回的引用必须 outlive this frame

Heap is a pool of memory,堆内存中的值需要明确的分配和释放,使 frame 中的值 outlive 可以把这个值放在堆上,线程之间不共享 stack,堆上的值可以夸线程。Rust 中主要通过 Box 分配 heap

Static memory 会被编译进二进制文件中,整个程序执行过程中都存在,如常量(const)、字符串(&'static str)。'static 生命周期表示直到程序关闭都有效,std::thread::spawn

Rust 中常见的有关生命周期的误解 - 2. 如果 T: 'static 那么 T 直到程序结束为止都一定是有效的

Ownership

所有的值都有一个所有者,负责释放

Borrowing and Lifetimes

Shared References

&T

impl<'_, T> Copy for &'_ T

Primitive Type reference - Trait implementations

Mutable Exclusive Reference

&mut T

dtolnay/macro._02__reference_types

Interior Mutability

通常依靠额外的机制(如原子 CPU 指令、runtime check)或不变性来提供安全的可变性,而不依赖 &mut T。这些类型通常分为两类:一类是让你通过共享引用获得一个可变的引用,另一类是让你替换一个只给定共享引用的值

Lifetimes

Lifetimes and the Borrow Checker,就像之前通过 flow 进行 borrowck

Generic Lifetimes:

1struct StrSplit<'s, 'p> {
2 delimiter: &'p str,
3 document: &'s str,
4}
5
6impl<'s, 'p> Iterator for StrSplit<'s, 'p> {
7 type Item = &'s str;
8
9 fn next(&self) -> Option<Self::Item> {
10 todo!()
11 }
12}
13
14fn str_before(s: &str, c: char) -> Option<&str> {
15 StrSplit { document: s, delimiter: &c.to_string() }.next()
16}

Lifetime Variance:covariant(协变), invariant(不变), and contravariant(逆变),&'a T 和 &'a mut T 对于 'a 来说都是协变的,对 T 来说就不谈了,因为 “Subtyping in Rust is very restricted and occurs only due to variance with respect to lifetimes and between types with higher ranked lifetimes.

Types

Types in Memory

Alignment

为了减少硬件读取内存的次数需要对齐,所以类型所占内存的大小都得是其 align 的倍数,u8 是一字节对齐,u16 两字节对齐,复杂类型按包含类型的最大的对齐方式对齐

Layout

1#[repr(C)]
2struct Foo {
3 tiny: bool, // 1
4 // _p1: [0; 3], // 3
5 normal: u32, // 4
6 small: u8, // 1
7 // _p2: [0; 7] // 7
8 long: u64, // 8
9 short: u16, // 2
10 // _p3: [0; 6] // 6
11}
12
13std::mem::align_of::<Foo>(); // 8
14std::mem::size_of::<Foo>(); // 32

repr(C) 的布局是这样的,repr(Rust) 的布局会进行各种优化,使其顺序改变,所以即使两个不同的类型共享所有相同的字段、相同的类型、相同的顺序,也不能保证它们的布局是一样的

1struct Foo {
2 tiny: bool, // 1
3 normal: u32, // 4
4 small: u8, // 1
5 long: u64, // 8
6 short: u16, // 2
7
8 // long: u64, // 8
9 // normal: u32, // 4
10 // short: u16, // 2
11 // tiny: bool, // 1
12 // small: u8, // 1
13}
14
15std::mem::align_of::<Foo>(); // 8
16std::mem::size_of::<Foo>(); // 16

Visualizing memory layout of Rust's data types

Dynamically Sized Types and Wide Pointers

Sized 是一个 auto trait,因为太常用了大部分类型都实现了它,除了 trait object 和 slice,他们的大小在运行时才能知道,编译时推断不出来,这些类型需要放在指针后面(&[u8]Box<dyn Iterator>

Traits and Trait Bounds

Compilation and Dispatch

讲的 static dispatch 和 dynamic dispatch

Generic Traits

Rust 使 trait 变得 generic 的方式主要有两种:泛型参数(trait Foo<T> { ... })、关联类型(trait Foo { type Item; ... }

如果只希望类型对 trait 的实现只有一个,就用关联类型,否则用泛型参数,比如 IteratorIterator::Item 只能有一个,From<T> 可以有多个

Coherence and the Orphan Rule

为了明确类型的方法的实现是哪个,防止类似自己 impl Display for bool 影响其他 crate 中 bool 使用的情况,Rust 提出孤儿原则:只有当一个 trait 或 type 属于你的 crate 时,你才能为该 type 实现该 trait

孤儿原则也有些额外的影响:

  1. 允许 impl<T> MyTrait for T where T: ... 这种适用广泛类型的实现,但添加时为 brake change,可能会导致下游 crate 使用的方法冲突而无法编译
  2. 使用 #[fundamental] 属性的类型包括 &&mutBox,可以为其 impl MyTrait for &Foo
  3. impl From<MyType> for Vec<usize> 这种有一部分是允许的

Trait Bounds

这样是可以的,渐进式的确认类型很有帮助

1fn foo(s: String) -> String where String: Clone {
2 s.clone()
3}

HRTB:If you write F: Fn(&T) -> &U, you need to provide a lifetime for those refer- ences, but you really want to say “any lifetime as long as the output is the same as the input.” Using a higher-ranked lifetime, you can write F: for<'a> Fn(&'a T) -> &'a U to say that for any lifetime 'a, the bound must hold. The Rust compiler is smart enough that it automatically adds the for when you write Fn bounds with references like this, which covers the majority of use cases for this feature.

Marker Traits

包括 Copy、Send、Sync、Sized、Unpin,同时除了 Copy 都是 auto trait

Unit Type 的一个例子,状态模式:

1use std::marker::PhantomData;
2
3struct Authenticated;
4struct Unauthenticated;
5struct SshConnection<S> {
6 _marker: PhantomData<S>,
7}
8
9impl<S> SshConnection<S> {
10 pub fn new() -> SshConnection<Unauthenticated> {
11 SshConnection::<Unauthenticated> { _marker: PhantomData }
12 }
13}
14
15impl SshConnection<Unauthenticated> {
16 pub fn connect(&mut self) -> SshConnection<Authenticated> {
17 SshConnection::<Authenticated> { _marker: PhantomData }
18 }
19}

Existential Types

1fn foo() -> impl Future<Output = i32> {}
1#![feature(type_alias_impl_trait)] // see https://github.com/rust-lang/rust/issues/63063
2
3struct Foo {
4 v: Vec<i32>,
5}
6
7impl IntoIterator for Foo {
8 type Item = i32;
9 type IntoIter = impl Iterator<Item = Self::Item>;
10
11 fn into_iter(self) -> Self::IntoIter {
12 self.v.into_iter()
13 }
14}

Designing Interfaces

See also:

Unsurprising

Naming Practices

into、as、to_、iter、iter_mut、get、get_mut……

Common Traits for Types

  1. Debug
  2. Sync、Send、Unpin、Sized auto-traits
  3. Clone、Default,如果不能实现要文档说明
  4. PartialEq 以便 assert_eq!
  5. PartialOrd、Hash 可能用于作为 key 时实现
  6. Eq、Ord 语义有额外要求,符合时实现
  7. serde 的 Serialize 和 Deserialize,不想添加必要依赖可开 feature “serde” 提供
  8. 尽量不实现 Copy,破坏 move 语义,而且去掉 Copy 是 break change

Ergonomic Trait Implementations

1trait Foo {
2 fn foo(&self);
3}
4
5impl<T> Foo for &T where T: Foo {
6 fn foo(&self) {
7 Foo::foo(*self);
8 }
9}
10
11impl<T> Foo for &mut T where T: Foo {
12 fn foo(&self) {
13 Foo::foo(*self);
14 }
15}
16
17impl<T> Foo for Box<T> where T: Foo {
18 fn foo(&self) {
19 Foo::foo(&**self);
20 }
21}
22
23struct Fo;
24
25impl Foo for Fo {
26 fn foo(&self) {
27 println!("foo");
28 }
29}
30
31fn fooo<T>(f: T) where T: Foo {
32 f.foo();
33}
34
35fn main() {
36 let f = &Fo;
37 fooo(f);
38 let f = &mut Fo;
39 fooo(f);
40 let f = Box::new(Fo);
41 fooo(f);
42}

Wrapper Types

  • Deref:方便 . 调用 Target 上的方法
  • AsRef:方便 &Wrapper 作为 &Inner 使用
  • From<Inner>From<Wrapper>:方便添加和删除这层包装

Borrow 只适用于你的类型本质上等同于另一个类型的情况,如 &String 和 &str

Flexible

Generic Arguments

static dispatch 容易使类型变的复杂,有时可以用 dynamic dispatch 代替以减少复杂度,不过需要考虑

  • dynamic dispatch 有性能损耗
  • 复杂的情况下 Rust 不知道如何构建 vtable(&dyn Hash + Eq)
  • static dispatch 的代码用户可以自己调用时传入 trait object 以 dynamic dispatch 的方式使用
1fn foo<T: Debug>(f: T) {
2 dbg!(f);
3}
4
5fn main() {
6 let d: &dyn Debug = &"hah".to_owned();
7 foo(d);
8}

所以对 lib 来说 static dispatch 的接口更好,app 来说因为是最下游两种都可以

一开始使用具体类型之后逐渐改为泛型是可行的,但并不一定都是向后兼容的:

1fn foo(v: &Vec<i32>) {}
2// =>
3fn foo(v: impl AsRef<[i32]>)
4// 虽然 Vec<T> 实现了 AsRef<T>,但用户可能会这样调用:
5foo(&iter.collect()) // 导致 collect 的类型推断失效

Object Safety

trait 的设计应该考虑到是否有 trait object 的场景,一般倾向于需要实现,因为增加了 dyn 的使用方式

  • 泛型方法上的泛型可不可以放到 trait 上,以保证 object safety
  • 可以为不需要 dyn 的方法添加 Self: Sized

object safety 是 API 的一部分,需注意兼容性

Borrowed vs. Owned

API 对数据的 Owned 和 Borrowed 要仔细判断

Fallible and Blocking Destructors

一些 I/O 的 destructor 可能阻塞甚至失败,需要显式的解构,Option::takestd::mem::takeManuallyDrop 可能会比较有用

Obvious

有时我们的类型需要先调用 foo 然后再调用 bar,但用户并不知道

Documentation

  • 会 panic 的函数写明 Panic
  • Err 返回的原因
  • 对于 unsafe 的函数写明 Safety
  • examples 质量,用户很可能复制这里的代码
  • 组织文档,文档内链接,#[doc(hidden)] 标记不想公开的接口
  • #[doc(cfg(..))] 标记某些情况下才会用到的接口,#[doc(alias = "...")] 方便搜索

Type System Guidance

newtype、enum…… 实现 semantic typing

zero-sized type 表示:

1struct Grounded;
2struct Launched;
3
4struct Rocket<Stage = Grounded> {
5 stage: std::marker::PhantomData<Stage>,
6}
7
8impl Default for Rocket<Grounded> {}
9
10impl Rocket<Grounded> {
11 pub fn launch(self) -> Rocket<Launched> {}
12}
13
14impl Rocket<Launched> {
15 pub fn accelerate(&mut self) {}
16 pub fn decelerate(&mut self) {}
17}
18
19impl<Stage> Rocket<Stage> {
20 pub fn color(&self) -> Color {}
21 pub fn weight(&self) -> Kilograms {}
22}

Constrained

向后兼容

Type Modifications

尽量少的 pub 给用户会帮助我们控制代码

#[non_exhaustive] 可以避免用户 match、构造等可能需要枚举类型所有属性的操作,等类型稳定后请避免使用它

Trait Implementations

trait 的修改往往是 break 的

1pub trait CanUseCannotImplement: sealed::Sealed {
2 // ...
3}
4
5mod sealed {
6 pub trait Sealed {}
7 impl<T> Sealed for T where T: TraitBounds {}
8}
9
10impl<T> CanUseCannotImplement for T where T: TraitBounds {}

Error Handling

Representing Errors

Enumeration

  1. 实现 std::error::Error
  2. 实现 std::fmt::Display
  3. 尽量实现 Send、Sync,把 Rc、RefCell 放到 Error 中时需要考虑 Error 是否需要跨线程

see std::io::Error

Opaque Errors

并不是所有 lib 都适合 Enumeration 这种方案,对于错误原因并不重要的情况更适合用不透明的 Error 表示,比如:一个图像解码库,解码失败时图像头中的大小字段无效或者压缩算法未能解压一个块这种具体的原因对用户来说也许并不重要,因为即使知道了也无法恢复错误,而不透明错误使库更容易使用,大大减小 API 复杂度(内部可以细化,但没必要暴露给用户)

通常是 Box<dyn Error + ...> 这样的,但 Box<dyn Error + Send + Sync + 'static> 可以使用户使用 Error::downcast_ref 特化 Error 进行处理

Propagating Errors

? 其实就是 std::Ops::Try trait,不过目前来没有稳定

try { ... } try block 的场景,不过也没稳定:

1fn do_it() -> Result<(), Error> {
2 let t = Thing::setup();
3 t.work()?; // Err 后会直接返回,没有 cleanup
4 t.cleanup();
5 Ok(())
6}
7
8fn try_it() -> Result<(), Error> {
9 let t = Thing::setup();
10 let r = try { t.work()? };
11 t.cleanup();
12 r
13}

Project Structure

Features

添加 optional 的 crate 和改变代码,以开启额外功能

Defining and Including Features

Cargo 中可以定义 features,默认使用的可以定义为 default

Cargo 使每个可选依赖关系都成为与依赖关系同名的 features,所以会有命名冲突

1[features]
2derive = ["syn"]
3
4[dependencies]
5syn = { version = "1", optional = true }

也可以开启依赖的一些 features

1[features]
2derive = ["syn/derive"]
3
4[dependencies]
5syn = { version = "1", optional = true }

Using Features in Your Crate

#[cfg(feature = "some-feature")]cfg!(feature = "some-feature)) 来控制 conditional compilation

Testing

Rust Testing Mechanisms

#[test], #[should_panic], #[cfg(test)], Integration tests (the tests in tests/), compile_fail in doctests, # in doctests...

Additional Testing Tools

  • clippy
  • cargo-fuzz
  • miri
  • loom

Marcos

Declarative Macros

When to Use Them

当你发现自己反复编写相同的代码时,声明宏会很有用

How They Work

parse 的时候遇到声明宏会根据定义进行展开(定义必须在调用之前解析),由于是 Rust 编译器进行解析,所以宏的语法必须是编译器可以识别的 token

How to Write Declarative Macros

Rust 的宏是卫生的,一个声明宏(通常)不能影响那些没有明确传递给它的变量

1macro_rules! let_foo {
2 ($x:expr) => {
3 let foo = $x;
4 }
5}
6
7let foo = 1;
8let_foo!(2);
9assert_eq!(foo, 1);

宏的调用不能受调用位置影响,::std::core$crate 进行导入

宏只有声明后才存在,如果一个 mod 想用另一个 mod 中的宏,那么声明这个宏的 mod 必须放在另一个 mod 前面;或者使用 #[macro_export] 标记宏,这会将宏提升到 crate 的根部并标记为 pub 的,这样就可以在任何地方或其他依赖中使用

Procedural Macros

fn(TokenStream) -> TokenStream

Types of Procedural Macros

  • function-like macros, macro_rules!
  • attribute macros, #[test]
  • derive macros, #[derive(Serialize)]

The Cost of Procedural Macros

增加了几个大库,过程宏生成的代码,导致编译时间增加

So You Think You Want a Macro

派生宏 derive macros:实现 derive,很多类型都需要自动实现 trait 的时候使用

function-like macros:已有声明宏但是越来越难维护,或者有的想在编译期计算而 const 实现不了

How Do They Work?

AST 层面进行操作

Asynchronous Programing

What’s the Deal with Asynchrony?

同步接口 - 多线程

异步接口 - Standardized Polling (Future)

Ergonomic Futures

async/await(和 generator fn)会被编译为有限状态机,以实现零成本抽象 RustLatam 2019 - Without Boats: Zero-Cost Async IO