Away0x's Blog

Coding blogging for hackers.

Rust - Pattern Match

  • 模式是 Rust 的一种特殊语法,用于匹配复杂和简单类型的结构
  • 将模式与匹配表达式和其他构造结合使用,可以更好地控制程序的控制流
  • 模式由以下元素 (的一些组合) 组成:
    • 字面值
    • 解构的数组、enum、struct 和 tuple
    • 变量
    • 通配符
    • 占位符
  • 想要使用模式,需要将其与某个值比较
    • 如果可以模式匹配,就可以在代码中使用这个值的相应部分

可用用到模式匹配的地方

match 的分支

  • 要求分支需要包含所有的可能性
    • _: 可以匹配任何东西,不会绑定到变量,通常用于 match 的最后一个分支,或用于忽略某些值
1
2
3
4
5
match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

if let

  • 简单的控制流表达式,处理只关心一种匹配而忽略其他匹配的情况,放弃了穷举的可能性,可以理解成 match 的简化版
  • 主要是作为一种简短的方式来等价的代替只有一个匹配项的 match
  • if let 可选的可以拥有 else else if else if let
  • if let 不会检查穷尽性
1
2
3
4
5
6
7
8
9
10
11
12
13
let v = Some(0u8);

match v {
    Some(3) => println!("three"),
    _ => println!("others"),
}

// 可使用 if let 改写
if let Some(3) = v {
    println!("three");
} else {
    println!("others");
}

while let 条件循环

  • 只有模式继续满足匹配的条件,那它运行 while 循环一直运行
1
2
3
4
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{}", top);
}

for 循环

1
2
3
4
let arr = vec!['a', 'b', 'c'];
for (i, v) in arr.iter().enumerate() {
    println!("{}: {}", i, v);
}

let 语句

1
2
let a = 5; // 这个其实也是一个模式
let (x, y, z) = (1, 2, 3);

函数参数

1
2
3
4
5
fn foo(x: i32) {} // 参数 x 也是一个模式
fn foo2(&(x, y): &(i32, i32)) {}

let point = (3, 5);
foo2(&point);

模式的可辩驳性

  • 模式有两种形式: 可辩驳的(可失败的), 无可辩驳的(不可失败的)
    • 能匹配任何可能传递的值的模式: 无可辩驳的
      • 例如: let x = 5
    • 对于某些可能的值,无法继续匹配的模式: 可辩驳的
      • 例如: if let Some(x) = val
  • 函数参数、let 语句、for 循环只接收无可辩驳的模式
  • if letwhile let 接收可辩驳无可辩驳的模式
  • match 语句,除了最后一个分支是无可辩驳的,其他分支为可辩驳

匹配语法

匹配字面值

1
2
3
4
5
6
7
8
let x = 1;

match x = {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

匹配命名变量

  • 命名的变量是可匹配任何值的无可辩驳模式
1
2
3
4
5
6
7
8
9
10
11
12
let x = Some(5);
let y = 10;

match x = {
    Some(50) => println!("Got 50"),
    Some(y) => println!("Matched, y = {:?}", y), // 作用域只在子句中
    _ => println!("Default case, x = {:?}", x),
}

println!("at the end: x = {:?}, y = {:?}", x, y);
// "Matched, y = 5"
// "at the end: x = Some(5), y = 10"

多重模式

  • match 可使用 |(或运算符), 可以匹配多种模式
1
2
3
4
5
6
7
let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

匹配范围

1
2
3
4
5
6
let x = 5;

match x {
    1..=5 => println!("one through five"), // 1,2,3,4,5
    _ => println!("something else"),
}

使用模式来解构分解值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 解构 struct
struct Point { x: i32, y: i32, }

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);

    match p {
        // 子分支会创建 x y 变量
        // 匹配 x = any, y = 0 的 Point
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        // 匹配 x = 0, y = any 的 Point
        Point { x: 0, y } => println!("On the y axis at {}", y), // 匹配到该行
        // 匹配 x = any, y = any 的 Point
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 解构 enum
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit => {...}
        // 会创建 x y 变量
        Message::Move { x, y } => {...}
        Message::Write(text) => {...},
        Message::ChangeColor(r, g, b) => {...}
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 解构嵌套的类型
enum Color {
   Rgb(i32, i32, i32),
   Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => {...}
        Message::ChangeColor(Color::Hsv(h, s, v)) => {...}
        _ => ()
    }
}
1
2
// 解构 tuple
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

在模式中忽略值

  • _: 可用来忽略某个值
  • 使用以 _ 开头的名称: 标记该值不会被用到
  • ..: 忽略值的剩余部分
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
// 使用 `_` 来作为匹配但不绑定任何值的通配符
fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}

// --------
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}
println!("setting is {:?}", setting_value);

// --------
let numbers = (2, 4, 8, 16, 32);
match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth)
    },
}
1
2
3
4
5
6
7
8
9
10
// 使用以 `_` 开头的名称: 忽略被使用的变量
let _x = 5; // _x 没被使用也不会有警告

// -------
let s = Some(String::from("Hello!"));
// s 多所有权被移入了 if let 子句了, _s 会发生绑定操作获取所有权
// 如果将 Some(_s) 修改为 Some(_),_ 不会发生绑定操作,所以 s 的所有权也不会移动了
if let Some(_s) = s {
    println!("found a string");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 用 .. 忽略剩余值
struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {}", x),
}

// ----------
let numbers = (2, 4, 8, 16, 32);

match numbers {
    // (.., second, ..) 这种模糊的匹配会失败
    (first, .., last) => {
        println!("Some numbers: {}, {}", first, last);
    },
}

match guard

1
2
3
4
5
6
7
8
9
10
11
// 指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支
let x = Some(5);
let y = 10;

match x {
    Some(50) => println!("Got 50"),
    // 匹配 Some(n), 然后绑定 n, 如果 n == y, 匹配成功
    // 该例中, 匹配 Some(n) 成功,但是由于 n != y,所以进入 _ 分支
    Some(n) if n == y => println!("Matched, n = {}", n),
    _ => println!("Default case, x = {:?}", x), // 最终匹配到这个分支
}
1
2
3
4
5
6
7
8
let x = 4;
let y = false;

match x {
    // 匹配 4, 5, 6; 并且 y == true
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"), // 最终匹配到这个分支
}

@ 绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @ 符号让我们可以创建一个变量,该变量可以在测试某个值是否与模式匹配的同时保存该值
enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    // 要求 id 的值在 3~7 范围内,同时使用 @ 将匹配到的值存储在 id_variable 变量中
    Message::Hello { id: id_variable @ 3..=7 } => {
        println!("Found an id in range: {}", id_variable) // 
    },
    // 这里由于 id 匹配的是一个范围,其可能是 10 11 12, 没有具体的值,所以即使匹配到了也无法绑定
    // 如果匹配到该子句了,想在子句内使用具体匹配到的值,也得 { id: id_variable @ 10..=12 }
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    },
    // 该子句指定了一个没有范围的比例,所以如果匹配到这行,id 可以绑定具体的值
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}
// Found an id in range: 5

Rust - Concurrency

  • Concurrent 并发: 程序的不同部分之间可以独立的运行
  • Parallel 并行: 程序的不同部分同时运行

线程

  • 通过 thread::spawn 创建线程
    • 参数为一个闭包, 闭包里面是在新线程中运行的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
    // 当主线程结束时,新线程也会结束,而不管子线程是否执行完毕
    // 可以使用 join 等待子线程执行完毕 (会堵塞, 直到线程执行完毕)
    handle.join().unwrap();
}
  • move 闭包常与 spawn 一起使用, 它允许我们在一个线程中使用另一个线程的数据
1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
    let v = vec![1, 2, 3];
    // 将 v 的所有权移动到了线程中, 这样线程中就可以安全的使用 v 变量了
    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

消息传递

  • 线程 (或 Actor) 通过彼此发送消息(数据)来进行通信
    • 不用用共享内存来通信, 要用通信来共享内存
  • Rust 中使用 Channel 来实现消息传递
    • Channel 包含发送端和接收端, 如果这两端中任意一端被丢弃了, 那么 Channel 就关闭了
  • 使用 mpsc::channel 来创建 Channel
    • mpsc (multiple producer, single consumer): 多个生产者, 一个消费者
  • 发送端:
    • send 方法:
      • 参数: 要发送的数据
      • 返回值: Result<T, E>, 如果有问题(如接收端已经被丢弃), 就会返回一个 Err
  • 接收端:
    • recv 方法 (堵塞当前线程运行, 直到 Channel 中有值传送过来)
      • 一但有值收到, 就会返回 Result<T, E>
      • 当发送端关闭, 就会收到一个错误
    • try_recv 方法 (不会堵塞)
      • 立即返回 Result<T, E>, 有数据到达会返回 Ok, 否则返回 Err
      • 通常会使用循环调用来检查该函数的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::thread;
use std::sync::mpsc;

fn main() {
    // tx: 发送端
    // rx: 接收端
    let (tx, rx) = mpsc::channel();

    // move: 使新的线程拥有了 tx 发送端的所有权了
    thread::spawn(move || {
        let val = String::from("hi");
        // 这里 send 之后, val 的所有权会被转移给接收者
        tx.send(val).unwrap(); // 发送数据
        // val 不可用了
    });

    // 接收数据 (阻塞主线程执行直到从通道中接收一个值)
    // 当通道发送端关闭,recv 会返回一个错误表明不会再有新的值到来了
    // 还有个方法 try_recv, 其不会堵塞则是立即返回 Result, Ok 表示有值, Err 表示值还没到来
    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}
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
// 发送多个值
use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1)); // 暂停 1s
        }
    });

    // 可把接受者 rx 当作迭代器使用, 每过 1s 会接收到一个值
    for received in rx {
        println!("Got: {}", received);
    }
}
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
// 通过克隆发送者来创建多个生产者

// ...
let (tx, rx) = mpsc::channel();

let tx1 = tx.clone();
thread::spawn(move || {
    let vals = vec![
        String::from("hi"),
        String::from("from"),
    ];

    for val in vals {
        tx1.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

thread::spawn(move || {
    let vals = vec![
        String::from("more"),
        String::from("messages"),
    ];

    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

// 会接收到 tx1 和 tx 两个发送端发送的数据
for received in rx {
    println!("Got: {}", received);
}
// ...

共享状态

  • Rust 支持通过共享状态来实现并发
    • Channel 类似单所有权: 一旦值的所有权转移至 Channel, 就无法使用它了
    • 共享内存的并发类似多所有权: 多个线程可以同时访问同一块内存
  • 使用 Mutex (mutual exclusion 互斥锁) 来保证每次只允许一个线程来访问数据
    • 想要访问数据, 线程必须首先获取互斥锁 (lock)
      • lock: 是一种数据结构, 是 mutex 的一部分, 它能跟踪谁对数据拥有独占访问权
    • mutex 通常被描述为: 通过锁定系统来保护它所持有的数据
  • Mutex 的两条规则
    1. 在使用数据之前, 必须尝试获取锁(lock)
    2. 使用完 mutex 所保护的数据, 必须对数据进行解锁, 以便其他线程可以获取锁
  • Mutex 的 API:
    • 通过 Mutex::new(数据) 来创建 Mutex<T>
      • Mutex<T> 是一个智能指针
    • 访问数据前, 通过 lock 方法来获取锁
      • 会堵塞当前线程
      • lock 可能会失败
      • 返回的是 MutexGuard (智能指针, 实现了 Deref 和 Drop)
1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    } // mutex 使用了 Drop trait, 所以这里会自动 unlock

    println!("m = {:?}", m);
}
  • 原子引用计数 Arc<T> (A: atomic)
    • 类似 Rc<T> (Rc 线程不安全) 并可以安全的用于并发环境的类型, 和 Rc<T> 有着相同的 API
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
// 多线程共享 mutex
use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // 利用 Arc 实现多线程之间共享所有权
        // 不使用 Arc 的话, 在第一次循环时, counter 的所有权就会被移动到线程中, 第二次循环就会报错 (不能将 counter 锁的所有权移动到多个线程中)
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 线程中获取 lock, 并修改值
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    // 等所有线程都执行完毕后再往下执行
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

RefCell<T>/Rc<T>Mutex<T>/Arc<T> 的相似性

  • Mutex<T> 提供了内部可变性, 和 Cell 家族一样
    • 使用 RefCell<T> 来改变 Rc<T> 里面的内容
    • 使用 Mutex<T> 来改变 Arc<T> 里面的内容
    • 注意: Mutex<T> 有死锁风险

Send & Sync

Send Trait

  • std::marker::Send: 允许线程间转移所有权
  • 实现 Send Trait 的类型就可以在线程间转移所有权
    • Rust 中几乎所有的类型都实现了 Send
    • Rc<T> 没有实现 Send, 它只用于单线程场景
  • 任何完全由 Send 类型组成的类型也是 Send
  • 除了原始指针之外, 几乎所有的基础类型都是 Send

Sync Trait

  • std::marker::Sync: 允许从多线程访问
  • 实现 Sync Trait 的类型可以安全的被多个线程引用
    • 即: 如果 T 是 Sync, 那么 &T 就是 Send, &T 可以被安全的送往另外一个线程
  • 任何完全由 Sync 类型组成的类型也是 Sync
  • 基础类型都是 Sync
  • Rc<T>, RefCell<T>, Cell<T> 家族都不是 Sync 的
  • Mutex<T> 是 Sync 的

Rust - Smart Pointers

  • 指针: 一个变量在内存中包含的是一个地址 (指向其他数据)
  • Rust 中最常见的指针就是 “引用”
    • 使用 &
    • 借用它指向的值
    • 没有其余开销
    • 最常见的指针类型
  • 智能指针:
    • 行为和指针类似
    • 有额外的元数据和功能
  • 引用智能指针的不同
    • 引用: 只借用数据
    • 智能指针: 很多时候都拥有它所指向的数据
  • 常用的智能指针类型:
    • Box<T>: 在 heap 内存上分配值
    • Rc<T>: 启用多重所有权的引用计数类型
    • Ref<T>RefMut<T>: 通过 RefCell<T> 访问,在运行时而不是编译时强制借用规则的类型
  • StringVec<T> 就是智能指针
    • 它们都拥有一片内存区域,且运行用户对其操作
    • 拥有元数据 (例如容量等)
    • 提供额外的功能或保障 (String 保障其数据时合法的 UTF-8 编码)
  • 通常使用 struct 实现,并且实现了: DerefDrop 两个 Trait
    • Deref trait: 允许智能指针 struct 的实例像引用一样使用
    • Drop trait: 允许你自动移当智能指针实例走出作用域时的代码
  • 内部可变模式 (interior mutability pattern): 不可变类型暴露出可修改其内部值的 API

Box<T> 类型

  • 其是最简单的智能指针:
    • 允许你在 Heap 上存储数据 (而不是 stack)
    • stack 上是指向 Heap 数据的指针
    • 没有性能开销
    • 没有其他额外功能
    • 适用于 “间接” 存储的场景
    • 实现了 DerefDrop 两个 Trait
  • 使用场景:
    1. 在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小
      • Box<T> 是一个指针, Rust 知道它需要多少空间 (指针的大小不会基于它指向的数据的大小变化而变化)
      • Box<T>
    2. 当有大量数据,想移交所有权,但需要确保在操作时数据不会被复制
    3. 使用某个值时,你只关心它是否实现了特定的 Trait,而不关心它的具体类型
1
2
3
4
fn main() {
    let a = Box::new(5); // 整数 5 存储在 heap 上了
    println("a = {}", a); // "a = 5"
} // a 被释放了 (会释放其存储在 stack 上的指针,以及存储在 heap 上的数据)

Cons List (递归类型)

  • 在编译时,Rust 需要知道一个类型所占的空间大小
  • 而递归类型的大小无法在编译时确定, 但 Box 类型的大小确定,在递归类型中使用 Box 就可解决上述问题
  • 这种类型 Cons List 是来自 Lisp 的一种数据结构
    • Cons List 里每个成员由两个元素组成
      1. 当前项的值
      2. 下一个元素
    • Cons List 里最后一个成员只包含一个 Nil 值,没有下一个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 Rust 实现 Cons List
use crate::List::{Cons, Nil};

enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    // Box<T> 提供了 "间接" 存储和 heap 内存分配的方式
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));

    println!("{:?}", list); // Cons(1, Cons(2, Cons(3, Nil)))
}

Deref Trait

  • 实现该 Trait 可使我们能自定义解引用运算符 * 的行为
  • 通过实现 Deref,智能指针可像常规引用一样来处理
  • 解引用运算符 *
    • 常规的引用也是一种指针
1
2
3
4
5
6
7
8
9
let x = 5;
let y = &x;

assert_eq!(5, x); // ok
assert_eq!(5, *y); // ok

// 使用 Box<T> 替代上面的引用
let y = Box::new(x);
assert_eq!(5, *y); // ok
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义智能指针
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl <T>std::ops::Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    assert_eq!(5, *y); // ok 
    // *y 相当于 *(y.deref())
}

Deref Coercion

  • Deref Coercion (函数和方法的隐式解引用转化) 是为函数和方法提供的一种便捷特性
  • 假设 T 实现了 Deref trait
    • Deref Coercion 可以把 T 的引用转化为 T 经过 Deref 操作后生成的引用
  • 当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:
    • Deref Coercion 就会自动发生
    • 编译器会对 deref 进行一系列调用,来把它转为所需的参数类型 (在编译时完成,没有额外的性能开销)
1
2
3
4
5
6
7
8
9
10
11
12
13
fn hello(name: &str) {
    println!("Hello, {}", name);
}

fn main() {
    let m = MyBox::new(String::from("world"));
    // &m: &MyBox<String>
    // - 由于 MyBox 实现了 deref trait,所以编译器可以把 &MyBox<String> 转化为 &String
    // - 由于标准库中 String 也实现了 deref trait,调用 deref 方法返回一个 &str 类型
    // - 所以 &m 可以作为参数传入,满足 &str 的参数类型要求
    hello(&m);
    // 如果没有实现 deref trait,则这里得这样调用: hello(&(*m)[..])
}

解引用与可变性

  • 可使用 DerefMut Trait 重载可变引用 * 运算符
  • 在类型和 trait 在下列三种情况发生时,Rust 会执行 Deref Coercion:
    1. T: Deref<Target=U>,允许 &T 转换为 &U
    2. T: DerefMut<Target=U>,允许 &mut T 转换为 &mut U
    3. T: Deref<Target=U>,允许 &mut T 转换为 &U

Drop Trait

  • 类似实现 Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作
    • 例如: 文件、网络资源释放等
  • 任何类型都可以实现 Drop Trait, 该 trait 在 prelude 里面
1
2
3
4
5
6
7
8
9
10
11
12
13
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    let a = CustomSmartPointer { data: String::from("hello") };
} // "drop"
  • Rust 不允许手动调用 Drop trait 的 drop 方法
    • 但可以调用标准库的 std::mem::drop 函数,来提前 drop 值

Rc<T> 引用计数智能指针

  • 有时, 一个值会有多个所有者, 为了支持这种多重所有权的情况, Rust 实现了 Rc<T>
    • RC: reference counting (引用计数)
    • 该类型会在实例内部维护一个用于记录值引用次数的计数器, 用于判断这个值是否被使用 (可以追踪值的所有引用)
    • 0 个引用: 该值可以被清理掉
    • Rc<T> 通过不可变引用, 使我们可以在程序不同部分共享只读数据
  • 使用场景: 需要在 heap 上分配数据, 这些数据被程序的多个部分读取(只读), 但在编译时无法确定哪个部分最后使用完这些数据
  • Rc<T> 只能用于单线程场景, 多线程场景可以使用 Arc<T>
    • Rc 为了性能使用的不是线程安全的引用计数器
    • Arc 内部引用计数使用了 atomic usize, 线程安全
  • Rc::clone(&a) 函数: 会增加引用计数
  • Rc::strong_count(&a): 获得引用计数
    • 还有 Rc::weak_count 函数
1
2
3
4
5
6
7
8
9
10
11
use std::rc::Rc;

fn main() {
    let a = Rc::new(1);
    // 对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数
    let b = a.clone();
    let c = a.clone();

    // 作用域有三个 Rc(a, b, c), 它们共同指向堆上相同的数据, 即堆上的数据有 3 个共享的所有者
    // 而当一个 Rc 结构离开作用域被 drop() 时,也只会减少其引用计数,直到引用计数为零,才会真正清除对应的内存
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 两个 List 共享另一个 List 的所有权
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    // b, c 共享 a 的所有权
    let b = Cons(3, Rc::clone(&a)); // 引用计数+1
    {
        let c = Cons(4, Rc::clone(&a)); // 引用计数+1
    } // c 离开作用域了, 引用计数自动 -1
    // a.clone() 该方法会进行深度的 copy; Rc::clone 只会增加引用计数, 不会执行数据的深度 copy
    // a 在引用计数清零的时候才会被回收
}

RefCell<T>

  • 内部可变性: 是 Rust 的设计模式之一, 它允许你在只持有不可变引用的前提下对数据进行修改
    • 数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则
  • Rc<T> 不同, RefCell<T> 类型代表了其持有数据的唯一所有权
    • Rc 是个只读的引用计数器, 无法拿到 Rc 结构内部数据的可变引用
  • RefCell<T>Box<T> 的区别
    • RefCell<T>: 只会在运行时检查借用规则, 运行时不满足借用规则会触发 panic
    • Box<T>: 在编译阶段强制代码遵守借用规则, 不满足规则会出现错误
  • 借用规则在不同阶段进行检查的比较:
    1. 编译阶段:
      1. 尽早暴露问题
      2. 没有任何运行时开销
      3. 对大多数场景是最佳选择
      4. 是 Rust 的默认行为
    2. 运行时:
      1. 问题暴露延后, 甚至到生产环境
      2. 由于使用了借用计数, 会产生些许性能损失
      3. 实现某些特定的内存安全场景 (不可变环境中修改自身数据)
  • Rc<T> 相似, 只能用于单线程场景
    • 多线程场景使用需要使用 Mutex(互斥量) 和 RwLock(读写锁)
    • 例如: Rc<RefCell<T>> 在多线程场景下可替换为 Arc<Mutex<T>>Arc<RwLock<T>>

内部可变性: 可变的借用一个不可变的值

  • 外部可变性: 使用 let mut, &mut
    • 所有权检查: 编译时, 如果不符合规则, 产生编译错误
  • 内部可变性: 使用 Cell, RefCell
    • 所有权检查: 运行时, 如果不符合规则, panic
1
2
3
4
5
6
fn main() {
    let x = 5;
    let y = &mut x; // 报错, 无法可变的借用一个不可变的值
    // 有时会需要有这一种场景, 值在外部是不可变的, 但是在方法内部需要可以修改这个值
    // 可以使用 RefCell 来实现这种内部可变性
}

使用 RefCell<T>

  • RefCell<T> 的方法
    • borrow 方法: 返回 Ref<T>, 它实现了 Deref trait
    • borrow_mut 方法: 返回 RefMut<T>, 它实现了 Deref trait
  • RefCell<T> 会记录当前存在多少个活跃的 Ref<T>RefMut<T> 智能指针
    • 每次调用 borrow: 不可变借用计数 +1
    • 任何一个 Ref<T> 的值离开作用域被释放时: 不可变借用计数 -1
    • 每次调用 borrow_mut: 可变借用计数 +1
    • 任何一个 RefMut<T> 的值离开作用域被释放时: 可变借用计数 -1
  • Rust 通过以上计数规则来维护借用的检查规则
    • 任何一个给定时间里, 只允许拥有多个不可变借用活一个可变借用
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(1);
    // 在同一个作用域下,我们不能同时有活跃的可变借用和不可变借用
    // 通过这对花括号,我们明确地缩小了可变借用的生命周期,不至于和后续的不可变借用冲突
    {
        // 获得 RefCell 内部数据的可变借用
        let mut v = data.borrow_mut();
        *v += 1;
    }
    println!("data: {:?}", data.borrow()); // data: 2
}

如何选择 Box<T>Rc<T>RefCell<T>

Box<T> Rc<T> RefCell<T>
同一数据的所有者 一个 多个 一个
可变性、借用检查 可变、不可变借用 (编译时检查) 不可变借用 (编译时检查) 可变、不可变借用 (运行时检查)

结合 Rc<T>RefCell<T> 来拥有多个可变数据所有者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

其他可实现内部可变性的类型

  • Cell<T>: 通过复制来访问数据
  • Mutex<T>: 用于实现跨线程情形下的内部可变性模式

Rust - Test

  • Rust 中一个测试就是一个函数, 需要使用 test 属性进行标注
  • 使用 cargo test 命令运行所有测试函数
    • Rust 会构建一个 Test Runner 可执行文件,它会运行标注了 test 的函数,并报告其运行是否成功

如何运行测试

1
2
3
4
5
6
7
8
9
10
11
# 并行运行所有测试函数
# - 测试成功时: 不显示所有输出(如 println!),使读取与测试结果相关的输出更容易
# - 测试失败时会显示如 println! 这样的输出
cargo test

# 命令行参数分为以下两大类
# 1. 针对 cargo test 的参数: 紧跟 cargo test 之后
cargo test --help # 输出 cargo test 的可用的参数

# 2. 针对测试可执行程序: 放在 -- 之后
cargo test -- --help # 输出 -- 之后的所有可用参数
  • 并行运行测试: 默认使用多个线程并行运行
    • 速度快
    • 要考虑并发影响,需要确保测试之间不会互相依赖,并且步依赖于某个共享状态(环境、工作目录、环境变量等等)
1
2
3
4
5
# --test-threads 参数
# - 传递给二进制文件
# - 🙅以并行方式运行测试,或向对线程数进行细粒度控制
# - 可使用该参数,后面跟线程的数量
cargo test -- --test-threads=1 # 单线程运行测试
  • 显示输出
    • 默认情况下, 测试通过,Rust 的 test 库会捕获所有打印到标准输出的内容 (如 println!)
1
2
# 即使测试通过,也显示标准输出
cargo test -- --show-output
  • 选择性的运行测试
1
2
3
4
# 根据测试函数的名称指定运行的测试
cargo test test_one_fn # 运行单个测试
# 可指定测试名的一部分 (模块名也可以) 来匹配并运行多个测试
cargo test test_ # 会运行测试函数名字中带有 test_ 的测试

如何编写测试

1
2
#[test]
fn test_fn() {}
  • assert! 宏,来自标准库,用来确定某个状态是否为 true
    • true: 测试通过
    • false: 调用 panic!,测试失败
  • 使用 assert_eq!assert_ne! 测试相等性
    • 判断两个参数是否相等/不等
    • 实际上,它们使用的就是 ==!= 运算符
    • 断言失败: 自动打印出两个参数的值 (要求参数实现了 PartialEq 和 Debug Traits)
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
#[derive(Debug)]
pub struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    pub fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle { length: 8, width: 7 };
        let smaller = Rectangle {
            length: 5,
            width: 1,
        };

        assert!(larger.can_hold(&smaller)); // 二参可传自定义信息
        // assert!(something, "{} {}", formart_param1, formart_param2);
    }
}
  • 使用 should_panic 属性测试是否发生了恐慌 (验证代码是否发生了 panic)
1
2
3
4
5
6
7
8
9
10
11
// 函数 panic: 测试通过,否则失败
#[cfg(test)]
mod tests {
    use super::*;

    // should_panic 修饰的函数 panic 了测试才会通过
    // 可 should_panic(expected = "xxxx"),验证 panic 时,错误信息是否包含了 expected 注明的字符串
    #[test]
    #[should_panic]
    fn test_fn() {...}
}
  • 测试中使用 Result<T, E>
    • 无需 panic, 可使用 Result<T, E> 作为返回类型编写测试
    • 返回 Ok 测试通过,返回 Err 测试失败
1
2
3
4
5
6
7
8
9
10
11
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(panic!(String::from("two plus two does not equal four")))
        }
    }
}
  • 忽略某些测试,运行剩余的测试
    • 使用 ignore 属性进行标记
1
2
3
4
5
6
7
8
9
10
11
// 运行 cargo test,只会运行 fn1 这个测试
// "cargo test -- --ignored": 只运行被标记 ignore 的测试
#[cfg(test)]
mod tests {
    #[test]
    fn fn1() {...}

    #[test]
    #[ignore]
    fn fn2() {...}
}

如何组织测试

单元测试

  • 一次对一个模块进行隔离的测试
  • 可测试 private 接口
  • 一般单元测试和被测试的代码都放在 src 目录下的同一个文件中
  • 约定每个源代码文件都建立 tests 模块来放测试函数,并使用 #[cfg(test)] 标注 tests 模块
    • 使用 #[cfg(test)] 标注后,只有运行 cargo test 才编译和运行代码,而 cargo build 则不会
    • cfg: Configuration, 告诉 Rust 下面的条目只有在指定的配置选项下才被包含
1
2
3
4
5
6
7
8
9
10
11
12
13
// 只在 cargo test 才会把以下代码拉入编译范围
fn fn1() {...}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        fn1(); // 可调用私有函数
        assert_eq!(4, 2 + 2);
    }
}

集成测试

  • 在库外部,和其他外部代码一样使用你的代码
  • 只能访问 public 接口
  • 可能在每个测试中使用到多个模块
  • 集成测试和被测试文件在不同的目录,不需要 #[cfg(test)] 标注
  • 集成测试放在 src 同级的 tests 目录下,tests 目录下的每个测试文件都是单独的一个 crate
    • 这些文件步共享行为 (与 src 下的文件规则不同)
    • 如果需要在 tests 文件下共享逻辑,可以建立子目录,在其中编写通用逻辑 (tests 下的子目录不会被当成测试文件运行)
1
2
3
4
5
6
7
8
// adder/tests/integration_test.rs
use adder; // 项目的名字

// 由于 tests 目录只会在执行 cargo test 命令的时候运行,所以不需要使用 #[cfg(test)] 标注
#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2))
}
  • 运行指定的集成测试 cargo test 测试函数名
  • 运行某个测试文件内的所有测试: cargo test --test 文件名

针对 binary crate 的集成测试

  • 如果项目时 binary crate,只含有 src/main.rs 没有 src/lib.rs
    • 不能在 tests 目录下创建集成测试
    • tests 无法把 main.rs 的函数导入作用域
  • 只有 library crate 才能暴露函数给其他 crate 用
  • binary crate 意味着独立运行
  • 所以通常 binary crate,都会把逻辑放在 lib.rs 里面,方便集成测试,main.rs 只有少量的调用逻辑

Rust - Lifetime

  • Rust 的每个引用都有自己的生命周期
  • 生命周期: 引用保持有效的作用域
  • 大多数情况: 生命周期是隐式的、可被推断的
  • 当引用的生命周期可能以不同的方式互相关联时: 需要手动标注生命周期

生命周期的标注语法

  • 生命周期的标注并不会改变引用的生命周期长度
  • 当指定了泛型的生命周期参数,函数可以接收带有任何生命周期的引用
  • 生命周期的标注: 描述了多个引用的生命周期间的关系,但不影响生命周期
  • 语法: 以 ' 开头, 例如: 'a
  • 标注位置
    • 在引用的 & 符号后
    • 使用空格讲标注和引用类型分开
  • 单个生命周期的标注本身并没有意义
1
2
3
&i32        // 一个引用
&'a i32     // 带有显示生命周期的引用
&'a mut i32 // 带有显示生命周期的可变引用
  • 函数签名中的生命周期标注
    • 泛型生命周期参数声明在: 函数名和参数列表之间的 <>
1
fn test<'a>(x: &'a str, y: &'a str) -> &'a str {...}

生命周期 - 避免悬垂引用 (dangling reference)

  • 生命周期的主要目标: 避免悬垂引用
  • Rust 编译器的借用检查器: 比较作用域来判断所有的借用是否合法
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
fn main() {
    {
        let r;                                      // ----------------+-- 'a
        {                                           //                 /
            let x = 5;                              // -+-- 'b         /
            r = &x;                                 //  /              /
        }                                           // -+--            /
        // 这里使用 r 时,r 所指向的 x 已经由于脱离作用域, //                /
        // 被销毁了,所以会报错                         //                /
        // 这里 Rust 编译器会使用借用检查器发现这种错误.   //                /
        println!("r: {}", r);                       //                 /
    }                                               // ----------------+--
}
// 上例 r 的生命周期为 'a,x 的生命周期为 'b
// 借用检查器在编译时发现 r 指向了生命周期为 'b 的一块内存 x,因为 'b 的生命周期比 'a 短,所以会编译失败
// 解决: 保证 x 的生命周期不小于 r 的生命周期 'a

fn main() {
    {
        let x = 5; // 移动 x 到 r 之前
        let r;
        {
            r = &x;
        }
        println!("r: {}", r); // x 的生命周期覆盖了 r 的生命周期,编译通过
    }
}

函数中的泛型生命周期

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
// 编译不通过,返回值包含了借用的值,但是**函数签名**没有说明这个借用的值是来自 x 还是 y
// - 编译器不晓得,返回类型的生命周期是和 x 还是和 y 一样
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 加上泛型生命周期,编译通过
// - 'a 的实际生命周期是: x y 两个生命周期中较小的那一个 (两个生命周期重叠的那一部分)
// - 'a 并不会改变传入的参数的生命周期,只是向借用检查器指出了一些可用于检查非法调用的约束而已
// - 所以 longest 并不需要准确的知道传入的变量 x y 的存活时长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 可编译通过
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";
    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
1
2
3
4
5
6
7
8
9
10
// 可编译通过
// 因为 string1 string2 在 result 被移出作用域之前都是有效的
fn main() {
    let string1 = String::from("abcd"); // 生命周期在 main 作用域内
    {
        let string2 = "xyz"; // &str 类型,拥有静态生命周期,在整个出现运行期间都存活
        let result = longest(string1.as_str(), string2); // 生命周期在当前这个内部作用域内
        println!("The longest string is {}", result);
    } // result 移出作用域
}
1
2
3
4
5
6
7
8
9
10
11
12
// 编译失败
// string2 存活时间不够长,其借用给了 result,但是生命周期比 result 短
// 这与 longest 函数签名注明的生命周期不一致
fn main() {
    let string1 = String::from("abcd"); // 生命周期在 main 作用域内
    let result; // 生命周期在 main 作用域内
    {
        let string2 = String::from("xyz"); // 生命周期在当前这个内部作用域内
        result = longest(string1.as_str(), string2.as_str());
    } // string2 移出作用域
    println!("The longest string is {}", result);
}
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
// pub fn strtok<'b, 'a>(s: &'b mut &'a str, delimiter: char) -> &'a str
// &'b mut: 指向字符串引用的可变引用 &mut 的生命周期
// &'a str: 字符串引用 &str 本身的生命周期
// 由于返回值只和 &str 有关, 所以可只需标注他的生命周期, 其他无关的生命周期可省略, 让编译器来自动添加
pub fn strtok<'a>(s: &mut &'a str, delimiter: char) -> &'a str {
    if let Some(i) = s.find(delimiter) {
        let prefix = &s[..i];
        // 由于 delimiter 可以是 utf8,所以我们需要获得其 utf8 长度,
        // 直接使用 len 返回的是字节长度,会有问题
        let suffix = &s[(i + delimiter.len_utf8())..];
        *s = suffix;
        prefix
    } else { // 如果没找到,返回整个字符串,把原字符串指针 s 指向空串
        let prefix = *s;
        *s = "";
        prefix
    }
}

fn main() {
    let s = "hello world".to_owned(); // str
    let mut s1 = s.as_str(); // &str
    let hello = strtok(&mut s1, ' '); // 访问 s1 的可变引用 
    println!("hello is: {}, s1: {}, s: {}", hello, s1, s); // 访问 s1 的不可变引用
    // print: "hello is: hello, s1: world, s: hello world"
}
// 注意!, strtok 生命周期标注不能写成如下样子
// 会导致 print s1(访问 s1 的不可变引用) 时报错:
// - cannot borrow `s1` as immutable because it is also borrowed as mutable
// 这是因为这样标注就表示可变借用 &mut 和 返回值的生命周期一样了, 导致这个引用的生命周期在函数结束后还没有结束
// 这样在 print s1 时, 就会发生可变引用和不可变引用同时存在的情况, 导致报错
/*
    let hello = strtok(&mut s1, ' '); // 访问 s1 的可变引用
    // 此时 s1 的生命周期 == hello 的生命周期
    println!("hello is: {}, s1: {}, s: {}", hello, s1, s); // 访问 s1 的不可变引用
*/
pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str

深入理解生命周期

  • 指定生命周期参数的方式依赖于函数所做的事
1
2
3
4
5
// 由于返回值的生命周期只和 x 的生命周期有关,所以 y 不用标注生命周期
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
  • 当函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
  • 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值
    • 这就是悬垂引用: 该值在函数结束时就走出了作用域
1
2
3
4
5
6
7
8
9
10
11
12
// 悬垂引用 (编译不通过)
// result 离开作用域了,这块内存就失效了
fn demo1() -> &str {
    let result = String::from("abc");
    result.as_str()
}

// 将所有权返回给调用方 (可编译通过)
fn demo2() -> String {
    let result = String::from("abc");
    result
}

Struct 定义中的生命周期标注

  • Struct 里可包括
    • 自持有的类型
    • 引用: 需要在每个引用上添加生命周期标注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ImportantExcerpt<'a> {
    // path 这个引用必须要比 ImportantExcerpt 的实例的存活要长
    part: &'a str,
}

// first_sentence 的生命周期能够覆盖 ie 的生命周期,所以编译通过
fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");

    let first_sentence = novel.split('.')
        .next()
        .expect("Could not found a '.'");

    let ie = ImportantExcerpt {
        part: first_sentence,
    };
}

生命周期的省略

  • 我们都知道
    • 每个引用都有生命周期
    • 需要为使用生命周期的函数或 struct 指定生命周期参数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 这个函数没有指定生命周期也能编译通过
// - 在 Rust 早期版本时无法编译通过的,当时要求每个引用都必须得有显式的生命周期
//   那时候这个函数得这么声明: fn first_word<'a>(s: &'a str) -> &'a str
// - 由于这类标注生命周期的规则都有相同的特征,所以编译器帮我们做了,不用程序员显式标注
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
  • 在 Rust 引用分析中所编入的模式称为生命周期省略规则
    • 这些规则无需开发者来遵守
    • 它们是一些特殊情况,由编译器来考虑
    • 如果你的代码符合这些情况,那么就无需显式标注生命周期
  • 生命周期的省略规则不会提供完整的推断:
    • 如果应用规则后,引用的生命周期仍然模糊不清,还是会导致编译错误
    • 解决办法: 添加生命周期标注,表明引用间的相互关系

生命周期省略的三个规则

  • 输入、输出生命周期
    • 函数/方法的参数: 输入生命周期
    • 函数/方法的返回值: 输出生命周期
  • 编译器使用三个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
    • 规则 1 应用于输入生命周期
    • 规则 2、3 应用于输出生命周期
    • 如果编译器应用完三个规则之后,仍然有无法确定生命周期的引用会编译报错
    • 这些规则适用于 fn 定义和 impl
  • 规则一: 每个引用类型的参数都有自己的生命周期
  • 规则二: 如果只有一个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
  • 规则三: 如果有多个输入生命周期参数,但其中一个是 &self&mut self (是方法), 那么 self 的生命周期会被赋给所有的输出生命周期参数
1
2
3
4
5
6
7
8
9
10
fn test(s: &str) -> &str {...}
// 应用第一条规则: fn test<'a>(s: &'a str) -> &str
// 应用第二条规则: fn test<'a>(s: &'a str) -> &'a str
// 可以确定生命周期了,不用程序员显式标注

fn test2(x: &str, y: &str) -> &str {...}
// 应用第一条规则: fn test2<'a, 'b>(x: &'a str, y: &'b str) -> &str
// 由于有多个参数,无法应用第二条规则
// 由于不是方法,无法应用第三条规则
// 无法确定生命周期,需要程序员自己显式标注

方法定义中的生命周期标注

  • 在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样
  • 在哪声明和使用生命周期参数,依赖于:
    • 生命周期参数是否和字段、方法的参数或返回值有关
  • struct 字段的生命周期名:
    • impl 后声明
    • struct 名后使用
    • 这些生命周期是 struct 类型的一部分
  • impl 块内的方法签名中
    • 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以
    • 生命周期省略规则经常使得方法中的生命周期标注不是必须的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    // 由于第一条规则: &self 可不用标注生命周期
    fn level(&self) -> i32 {
        3
    }

    // 由于第一条规则和第三条规则,可不用标注生命周期
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        self.part
    }
}

静态生命周期

  • 'static 是一个特殊的生命周期: 表示整个程序的持续时间
    • 例如: 所有的字符串字面值都有 'static 生命周期 (该类型是被直接存储在二进制文件中的)
      • let s: &'static str = "abc";
  • 为引用指定 'static 生命周期前要注意
    • 是否需要引用在程序整个生命周期内都存活

例子

  • 使用了使用泛型参数类型、Trait Bound、生命周期的例子
1
2
3
4
5
6
7
8
9
10
11
12
fn longest_with_an_announcement<'a, T>
    (x: &'a str, y: &'a str, ann: T) -> &'a str
where
    T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}