引用与借用

本页目录

借用让函数暂时访问值,而不接管其资源责任。&T 提供共享访问,&mut T 提供独占访问;引用还必须在使用期间指向有效、对齐且满足类型要求的数据。访问权限、有效期与地址条件缺一不可。

“多个读者或一个写者”是普通数据的直觉模型。内部可变性允许共享引用下按另一套协议修改,独占引用也可以被短暂重借用,因此不能把规则理解成所有语法上的引用变量绝不能共存。

Vec 元素借用为什么会阻止 push

push 需要独占访问 Vec,而且容量不足时可能重新分配,使旧元素地址失效。即使已预留容量,普通 push 接口仍要求 &mut Vec,不能在之后还要使用元素引用时绕过借用规则。

借用元素 first = &values[0]后续使用 first 时,元素必须保持可访问1最后一次读取 first借用的约束可在此后结束2随后修改 values此时可以取得不冲突的独占借用3借用约束按后续使用推导若把 first 的使用移到修改之后,借用范围也随之改变,程序可能被拒绝。
fn main() {
    let mut values = vec![1, 2, 3];
    let first = &values[0];
    assert_eq!(*first, 1);
    values.push(4);
    assert_eq!(values, [1, 2, 3, 4]);
}

把 assert_eq!(*first, 1) 移到 push 后面,会让 first 的借用跨过修改,从而产生编译错误。只写一个此后再未使用的引用绑定,则不能据此断言后面的修改一定非法。

NLL(非词法生命周期)按控制流与使用约束推导借用所需区域,不必保守地持续到词法块末尾。它不是简单文本扫描的“最后一行”,分支、返回引用与析构约束都可能影响结果。借用检查概述

把使用位置移过修改点

保持数据和 push 接口不变,只改变 first 的使用位置。橙色区间表示修改所需的独占访问,青色区间表示元素引用仍被后续操作需要。位置相交时,修正方向是改变使用时序或接口,而不是只增加容量。

正在呈现知识画面
把使用位置移过修改点

在这个直线程序中,push 后仍读取元素引用会产生借用冲突,即使预留容量也一样。

重借用:临时从已有权限派生更短权限

fn append(values: &mut Vec<i32>) { values.push(2); }
fn main() {
    let mut values = vec![1];
    let parent = &mut values;
    {
        let child = &mut *parent;
        append(child);
    }
    parent.push(3);
    assert_eq!(parent, &[1, 2, 3]);
}

parent 是已有的 &mut Vec,child 才是对其目标的重借用。在 child 被使用的期间,不能通过 parent 做冲突访问;child 的借用结束后,可以继续使用 parent。原 Vec 的所有权从未交出去,因此不是“函数返回后所有权恢复”。

已知参数类型为 &mut T 的调用常可隐式重借用,不需要每次显式写 &mut *parent。普通赋值或泛型按值消费不应一概认为都会重借用;&mut T 本身不是 Copy。

显式重借用与移动引用变量

fn consume<T>(_: T) {}
fn main() {
    let mut number = 1;
    let parent = &mut number;
    consume(&mut *parent); // 交出去的是新建的短借用
    *parent += 1;
    assert_eq!(*parent, 2);
}

如果把 consume(&mut *parent) 改为 consume(parent),泛型参数 T 会取为 &mut i32,传入的引用值本身被移动,后续 parent 不能再使用。两种写法都没有移动 number 的拥有权,区别是“哪一个引用值被消费”。修复时应让接口接收实际所需的 &mut T,或显式建立短借用,而不是为独占引用寻找 Clone。

独占借用可以拆成不相交部分

fn main() {
    let mut values = [1, 2, 3, 4];
    let (left, right) = values.split_at_mut(2);
    left[0] = 10;
    right[0] = 30;
    assert_eq!(values, [10, 2, 30, 4]);
}

两个 &mut 切片同时存在是安全的,因为元素范围不重叠。编译器也能识别一些结构体字段的独立借用;对一般的动态索引,则常需要 split_at_mut 等经过证明的接口,而不是期待它推导任意算术不相等条件。

动态选择两个元素,先把不相交关系变成切片边界

fn pair_mut<T>(values: &mut [T], i: usize, j: usize) -> Option<(&mut T, &mut T)> {
    if i == j || i >= values.len() || j >= values.len() { return None; }
    if i < j {
        let (left, right) = values.split_at_mut(j);
        Some((&mut left[i], &mut right[0]))
    } else {
        let (left, right) = values.split_at_mut(i);
        Some((&mut right[0], &mut left[j]))
    }
}
fn main() {
    let mut values = [10, 20, 30];
    let (a, b) = pair_mut(&mut values, 2, 0).unwrap();
    *a += 1;
    *b += 2;
    assert_eq!(values, [12, 20, 31]);
    assert!(pair_mut(&mut values, 1, 1).is_none());
    assert!(pair_mut(&mut values, 0, 3).is_none());
    assert!(pair_mut::<u8>(&mut [], 0, 0).is_none());
}

i < j 时在 j 处分开,i 一定位于左半,j 对应右半第零项;反向时交换返回顺序,使调用者始终先得到 i 再得到 j。这样既保留输入顺序,又通过安全切片 API 表达了互斥范围。业务只想交换两个位置时直接用 slice::swap 更简单,不必先取得两个引用。拆分借用

方法签名过宽也会制造不必要的冲突

struct Report { title: String, rows: Vec<usize> }
fn append_row(rows: &mut Vec<usize>, title: &str) {
    rows.push(title.len());
}
fn main() {
    let mut report = Report { title: String::from("demo"), rows: Vec::new() };
    let title = &report.title;
    append_row(&mut report.rows, title);
    assert_eq!(report.rows, [4]);
}

这里 title 和 rows 是不同字段,可以同时共享读取一个、独占修改另一个。如果改成接收 &mut Report 的方法,调用边界就声明它可能独占整个对象,调用者无法仅凭方法内部当前只修改 rows 来缩小这个要求。优先缩窄辅助函数参数,往往比增加 RefCell 或复制 title 更准确地表达职责。

引用变量不需要 mut,目标也可能被修改

let reference = &mut value; 的 reference 绑定没有 mut,仍可通过它修改目标。let mut reference 则额外允许把这个变量重新绑定到另一个引用。区分“指针变量可改”和“拥有独占目标访问权”有助于理解签名。

借用已结束不意味着 guard 已析构。RefCell 的 Ref、MutexGuard 等拥有运行时状态,通常需要在适当作用域退出或显式 drop 才释放相应协议状态,不能直接套用普通引用的 NLL 直觉。

返回引用与省略规则

fn first_word(text: &str) -> &str {
    &text[..text.find(' ').unwrap_or(text.len())]
}
fn main() {
    let text = String::from("中文 rust");
    assert_eq!(first_word(&text), "中文");
}

函数返回输入中的子切片,不返回局部新建 String 的借用。find 找到空格的字节位置正好处于 UTF-8 边界;不能任意把字符序号当字节偏移。

这里只有一个输入引用生命周期,输出按规则与它关联,不需要显式标注。多个输入存在歧义时才要表达更清楚的关系,见 生命周期。

String:"中文 rust"拥有文本与字节缓冲first_word(&text)返回同一缓冲中的 &str使用 word来源必须保持合法可读如果后面还要用 word,此前不能通过 text.push 修改来源返回借用视图:消费视图结束后才能独占修改来源若确实需要独立文本,显式 to_owned 创建 String;它改变拥有关系,也可能分配。

借用返回值会将约束带回调用方

fn first_word(text: &str) -> &str {
    &text[..text.find(' ').unwrap_or(text.len())]
}
fn main() {
    let mut text = String::from("中文 rust");
    let word = first_word(&text);
    assert_eq!(word, "中文");
    text.push('!'); // word 已不再使用

    let owned = first_word(&text).to_owned();
    text.clear();
    assert_eq!(owned, "中文");
}

&str 版本让结果与来源关联,适合短期读取;String 版本明确分离存储,适合需要跨越来源修改或释放的使用。后者不是“给引用延长生命周期”,而是创建了另一个拥有者负责的文本。

遇到借用错误时依次检查

问题对应处理
引用是否仍在修改之后使用将只读消费移到修改之前,或改变结果表示
两处操作是否实际针对不同字段/范围缩窄函数参数,使用拆分接口
是否移动了 &mut 引用本身使用明确参数类型或显式重借用
是否需要长期保存独立数据返回拥有值,明确复制或共享成本
是否其实是 guard 持有运行时锁限定 guard 作用域或显式 drop

这些处理分别改变访问时序、访问范围或拥有关系。使用 unsafe 绕过报错不会自动使关系成立,增加 clone 也不保证资源独立,必须先确定真正需要的契约。

编译期检查的保证范围

普通借用检查不在运行时追踪引用计数,也不提供任意资源的业务锁。&[T] 和 &dyn Trait 又包含相应元数据,不能统一说所有引用都固定 8 字节。进入 unsafe、FFI 或内部可变性封装后,额外契约由实现者维护;安全调用方依赖这些封装是 sound 的。