图书馆(库)无法弥补个人(程序员)能力的不足. --Mark Miller

  • 引用定义: reference, 非拥有型指针(不影响值的生命周期), 受到borrow checker的检查.
  • 引用约束: reference的生命周期不能超过所指向的值
  • 借用定义: borrow, 创建引用的特定术语, 强调引用的约束.
区别Rust 引用 &T / &mut TC++ 引用 T&C 指针 T*
是否可为空?, 不能是 null(受借用检查器保护), 不能是 nullptr, 可以是 NULL(可能导致空指针解引用)
是否需要解引用?, 直接使用,不需要 *, 直接使用,不需要 *, 需要 * 进行解引用
是否有独立地址?, 没有自己的地址, 没有自己的地址, 指针变量本身有地址
是否支持算术运算?, 不支持 +-, 不支持 +-, 支持指针运算(如 ptr++
生命周期管理, 受 借用检查 保护,防止悬垂指针, 可能悬垂(手动管理生命周期), 容易发生悬垂指针
适用场景安全的借用,确保数据有效主要用于参数传递指向动态内存、数组遍历

5.1 对值的引用

HashMap是标准库中的Hash表类型, 艺术家名称(String)映射其作品(Vec<string>), show函数用于打印Hash表中的内容.

核心问题: 根据move特性, 在HashMap没有实现Copy特型的情况下, 会将table的所有权move给show函数, table还原为未初始化, 且table在show函数结束后声明周期结束, 所有值drop. 如果后续还想继续使用table的话, 会失败.

解决方法: 使用reference, 它允许用户在不影响所有权的情况下访问值.

  • 共享引用: 只读, 数量任意, &e(类型: &T)产生共享引用, 实现了Copy特型
  • 可变引用: 可写, 数量唯一, &mut e(类型: &mut T)产生可变引用
  • 共享引用和可变引用互斥存在
  • 引用与拥有者的关系
    • 共享引用存在时: 拥有者也不能修改
    • 可变引用存在: 独占修改权和访问权, 拥有者也不能使用该值

5.1.a 艺术家程序 (所有权move)

use std::collections::HashMap;

type Table = HashMap<String, Vec<String>>;

fn show (table: Table) {
    for (artist, works) in table { // for获取table所有权, 并drop消耗
        println!("works by {}:", artist);
        for work in works {
            println!("    {}", work);
        }
    }
}

fn main() {
    let mut table = Table::new();

    table.insert("Gesualdo".to_string(), 
                vec!["many madrigals".to_string(),
                     "Tenebrae Responsoria".to_string()]);
    table.insert("Caravaggio".to_string(),
                 vec!["The Musicians".to_string(),
                      "The Calling of St. Matthew".to_string()]);
    table.insert("Cellini".to_string(),
                 vec!["Perseus with the head of Medusa".to_string(),
                      "a salt cellar".to_string()]);
    
    show(table);
}

运行

works by Caravaggio:
    The Musicians
    The Calling of St. Matthew
works by Gesualdo:
    many madrigals
    Tenebrae Responsoria
works by Cellini:
    Perseus with the head of Medusa
    a salt cellar

5.1.b 艺术家程序 (共享引用borrow)

主要修改

// 函数定义: 参数使用引用

fn show (table: &Table) -> () { // 使用引用
    for (artist, works) in table { // artist和works迭代的都是对应的共享引用
        println!("Artist: {}", artist);
        for work in works { // 迭代对元素的共享引用
            println!("    {}", work);
        }
    }
}

// 函数调用: 使用共享指针

show(&table); // 传入引用

5.2 使用引用

引用的应用:

  • 允许函数在不获取所有权的情况下访问或修改某个结构

5.2.1 显式/隐式解引用

特性说明
显式解引用算数运算, 比较运算, 传参时传入具体值, 都需要显式解引用
隐式解引用使用.时无需使用*, 因为编译器可以通过.确定这是一个引用

Rust显式解引用

let x = 10;
let r = &x; // &x是对x的共享引用
assert!(*r == 10); // 对r显式解引用

let mut y = 32;
let m = &mut y;
m += 

rust隐式解引用

    struct Anime { name: &'static str, bechdel_pass: bool } // 'static表示静态生命周期
    let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
    let anime_ref = &aria;
    assert_eq!(anime_ref.name, "Aria: The Animation"); // 隐式解引用
    assert_eq!(anime_ref.bechdel_pass, true);

    assert_eq!((*anime_ref).name, "Aria: The Animation"); // 显式解引用

5.2.2 对引用变量赋值

引用变量可以重新赋值, 指向新的值.

fn main() {
    let x = 10;
    let y = 20;
    let mut r = &x;

    r = &y;

    assert!(*r == 20);
}

5.2.3 对引用进行引用

  • Rust支持对引用进行引用 (类似于二重指针)
  • .运算符可以自动追踪多重引用来找到目标
    struct Point { x: i32, y: i32 }
    let point = Point { x: 1000, y: 729 };

    let r: &Point = &point;
    let rr: &&Point = &r;
    let rrr: &&&Point = &rr;

    assert_eq!(rrr.y, 729); // 三重引用只需要一次解引用
875

5.2.4 比较引用

类似于.自动多重解引用, Rust比较运算符也可以自动解引用追踪目标 (限制: 引用类型必须相同才能触发自动解引用)

如果比较的是引用本身(地址), 使用std::ptr::eq(rx, ry)比较

fn main() {
    let x = 10;
    let y = 10;

    let rx = &x;
    let ry = &y;

    let rrx = &rx;
    let rry = &ry;

    assert!(rrx <= rry); // 比较运算符自动解引用
    assert!(rrx == rry); // 比较运算符自动解引用
}

5.2.5 引用永不为空

Rust引用永远不为空 (防止悬空指针), 如果需要表示NULL, 使用Option<&T>来表示空指针, 因为该类型要求使用之前检查是否为None.

5.2.6 借用任意表达式结果值的引用

C++中只能对左值(拥有内存地址的变量)引用, Rust也可以对临时值引用

过程: * Rust检测到需要为一个立即数创建引用 * Rust为立即数创建一个匿名变量, 其生命周期取决于: * 如果立即将引用赋值给某个变量, 生命周期于引用相同 * 如果没有引用, 则生命周期在封闭语句块的末尾结束 (;结束) * Rust为匿名变量创建引用

fn factorial(n: usize) -> usize {
    (1..n+1).product() // 计算所有成绩
}

fn main() {
    let r = &factorial(6);
    assert_eq!(r + &1009, 1729); // 算数运算符也可以自动解引用
}

5.2.7 对切片和特型对象的引用

需要携带额外信息的引用, 即胖指针, 常见的两个胖指针:

  • 切片引用
  • 特型对象引用

5.3 引用安全

5.3.1 引用的生命周期

#Rust-point

引用的生命周期(作用域)必须在值的生命周期(作用域)之内

fn main() {
    let r;

    {
        let x = 1;
        r = &x; // x生命周期结束
    }

    assert_eq!(*r, 1); // 错误: 引用值的生命周期已经结束
}
特性说明
生命周期定义Rust为每个值, 每个引用分配生命周期, 并在编译期间检查 (编译完成后引用只是一个普通地址, 但是在编译阶段通过检查并确保了其安全性)
生命周期范围起点: 被初始化的点
终点: Rust编译器断定不再使用的点
三个生命周期1.值生命周期
2.引用变量生命周期: r引用变量本身初始化 -> r最后一次使用对应值引用 (注意: 是最后一次使用, 不是r本身drop, 因为r可以重新赋值)
3.引用生命周期: 上述实例中x借用的引用 (区分引用变量生命周期)
检查规则1.引用生命周期 ∈ 值生命周期 (否则产生悬空指针)
2.引用变量生命周期 ∈ 引用生命周期

总结: 引用变量生命周期 ∈ 引用生命周期 ∈ 值生命周期 (特别注意引用变量生命周期的定义)

推论(必要条件): 引用变量生命周期 ∈ 值生命周期

5.3.2 引用作为函数参数

static mut STASH: &i32; // 静态变量
fn f(p: &i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
    unsafe {
        STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
    }
}

// 错误一: 静态变量必须初始化
// 错误二: mut静态变量不是线程安全, 即使单线程也存在可重入问题 (所以static mut STASH只能在unsafe块中声明)

实际上rust省略了一部分的定义, 就是关于生命周期的定义.

'a表示生命周期是任意的:

  • 可以是很短的 (仅在f()调用期间),
  • 可以是很长的 (在调用者函数, 比如main()中)
  • 但不能自动提升为static, 除非显式要求.
  • p中存储的引用生命周期长度交给rust编译器来判断, 但是会确保最长不会超过函数本身生命周期
static mut STASH: &i32; // 静态变量
fn f<'a>(p: &'a i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
    unsafe {
        STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
    }
}

根据生命周期原则: 引用生命周期和值生命周期至少都应该包含引用变量生命周期, 而引用变量生命周期为static, 所以引用生命周期和值生命周期至少是staic

static mut STASH: &i32 = &10; // 静态变量
fn f(p: &'static i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
    unsafe {
        STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
    }
}

// 问题一解决: 初始化
// 问题二解决: 显式声明形参生命周期为static

5.3.3 引用传参

以下是错误实例: x生命周期在main()中, 最终的STASH引用变量生命周期是static, 已经超过了值生命周期, 所以编译错误.

static mut STASH: &i32 = &10; // 静态变量
fn f(p: &'static i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
    unsafe {
        STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
    }
}

fn main() {
    let x = 10;
    f(&x);
}

5.3.4 返回引用

下述代码功能: 返回最小元素的引用

fn smallest(v: &[i32]) -> &i32 {
    let mut s = &v[0];

    for r in &v[1..] {
        if *r < *s { s = r; }
    }
    s // 返回最小值引用
}

Rust的生命周期标注, 说明返回引用生命周期必须于参数v具有相同的生命周期

fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
    ...
}

fn main() {
    let s;
    {
        let parabola = [9, 4, 1, 0, 1, 4, 9];
        s = smallest(&parabola);
    }

    assert_eq!(*s, 0); // 错误: 返回引用的生命周期于&parabola相同, 不能超出{}
}

正确使用

fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
    let mut s = &v[0];

    for r in &v[1..] {
        if *r < *s { s = r; }
    }
    s // 返回最小值引用
}

fn main() {
    let s;

    let parabola = [9, 4, 1, 0, 1, 4, 9];
    s = smallest(&parabola);

    assert_eq!(*s, 0);
}

5.3.5 包含引用的结构体

结构体声明:

  • 结构体包含声明需要: 必须给出生命周期参数
  • 核心思想: 值, 引用, 引用变量的生命周期包含关系, 只不过引用变量隐藏在结构体内部.
struct S {
    r: &i32
}

fn main() {
    {
        let x = 10;
        s = S { r: &x };
    } // 值生命周期

    assert_eq!(*s.r, 10); // 引用变量生命周期超出值生命周期
}

给定生命周期参数, 决定'a生命周期长度的因素是: 存储在r中的引用生命周期, Rust会将'a生命周期完全限制在引用所指向的值的生命周期内部, 确保合法性.

struct S<'a> {
    r: &'a i32
}

将具有生命周期参数的类型放在其他结构中, 同样需要给定参数

// 更常用的写法

struct D<'a> {
    s: S<'a>
}

// 或者

struct E {
    s: S<'static>
}

5.3.6 生命周期参数

注意: 生命周期参数限定的是存储在引用变量中的引用生命周期长度.

  • 生命周期分析:
    • x的引用: 引用变量r和s.x均没有超过x值的生命周期
    • y的引用: s.y引用变量生命周期同样没有超过y的生命周期
  • 报错原因:
    • 结构声明时x和y成员的生命周期参数相同
    • s.x成员存储的引用是x引用, 中间借用给了r, 这就要求引用生命周期至少大于r的生命周期
    • s.y成员存储的引用是y引用, 中间借用给了s.y, 且引用生命周期不能大于y的值生命周期
    • 二者冲突导致编译错误
    • 解决方法: 使用两个生命周期参数来表示
struct S<'a> {
    x: &'a i32,
    y: &'a i32
}

fn main() {
    let x = 10;

    let r;

    {
        let y = 20;
        {
            let s = S { x: &x, y: &y };
            r = s.x;
        }
    }
}

5.3.7 省略生命周期参数

常见情况:

  • 自动标注: 无需返回引用或者其他带有生命周期参数的类型
  • 自动标注: 无歧义情况, 比如只有一个参数, 返回值会假设具有相同的生命周期
  • 自动标注: 实现一个struct的方法时, 默认从&self借用ref, 除非显示标注
  • 显式标注: 多个参数, Rust无法判断哪个生命周期作为返回值生命周期

5.4 共享与可变

悬空指针的产生原因:

  • 引用指向超出生命周期的变量 (上节讨论重点)
  • 引用指向的变量所有权move (本节讨论重点)
    • Rust解决方法: 引用存在的生命周期中, 引用目标保持只读, 不能move
// 非法操作

fn main() {
    let v = vec![4, 8, 19, 27, 34, 10];
    let r = &v; // r指向v
    let aside = v; // 错误: 引用生命周期内, Rust保持v只读
    r[0]; // r所指向的v未初始化
}

// 合法操作

fn main() {
    let v = vec![4, 8, 19, 27, 34, 10];
    {
        let r = &v;
        r[0];
    } // 引用生命周期结束

    let aside = v; // v恢复修改权限, 可以move
}
875

(实例) 切片扩展

//功能: 添加切片元素到向量中
fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
    for elt in slice {
        vec.push(*elt);
    }
}

fn main() {
    let mut wave = Vec::new();
    let head = vec![0.0, 1.0];
    let tail = [0.0, -1.0];

    extend(&mut wave, &head);
    extend(&mut wave, &tail);

    assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0]);
}

如果添加自身的元素, 假设capacity已满, 可能需要重新申请缓冲区:

  • slice引用变量仍然指向已经释放的缓冲区
  • vec指向新的缓冲区
  • 虽然vec是正确的, 但是很明显不能再解引用已经释放的slice, 导致错误

Rust将该问题简化: 不能同时出现共享引用和可变引用

    extend(&mut wave, &wave);
    assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0]);
875

基本引用规则

fn main() {
    let mut x = 0;
    let r1 = &x;
    let r2 = &x;

    x += 10; // 错误: 借出共享引用后, x只读
    let m = &mut x; // 错误: 共享引用和可变引用互斥存在

    println!("{}, {}, {}", r1, r2, m); // 最后一次使用, 即为引用变量生命周期
}

所有权树借用规则

fn main() {
    let mut v = (136, 139);
    let m = &mut v;
    let m0 = &mut m.0; // 允许对所有权树的子结点继续借用可变引用
    let r1 = & m.1; // 允许继续借用共享引用
    v.1; // 不允许通过除了可变引用以外的其他路径访问
}