Rust 借用与引用

上一篇讲了所有权,你可能会问:"每次调用函数都要丢一个变量,太麻烦了吧?"确实。所以 Rust 提供了借用(borrow)机制:用引用(reference)让你"借"值用,不拿走所有权。这是日常 Rust 编程 90% 的场景,务必掌握。

1. 引用:借而不取

引用用 & 操作符创建。把一个变量的引用传给函数,函数借用了它的值,用完还回来——原变量在调用后仍然有效:

fn main() {
    let s = String::from("hello");

    // 借用:传引用(用 & 操作符),不转移所有权
    let len = calculate_length(&s);
    println!("'{}' 的长度是 {}", s, len);  // s 仍然能用!
}

// 参数 s: &String 表示"借用一个 String"
// 函数用完不释放原数据(因为不拥有它)
fn calculate_length(s: &String) -> usize {
    s.len()
}   // s 离开作用域,但因为它是引用,什么都不会释放

// 对比:不用引用时(上一篇的写法)
// fn calc(s: String) -> usize { s.len() }
// 调用后原 s 失效——很麻烦

关键点:

引用在大多数情况下像普通值一样使用——Rust 会自动解引用(叫 deref coercion),你不需要写满星号。

2. 可变引用 &mut

默认的引用是不可变的——你只能读不能改。要修改借来的值,用可变引用 &mut:

fn main() {
    let mut s = String::from("hello");

    // 可变借用:&mut,允许函数修改借来的值
    append_world(&mut s);
    println!("{}", s);     // hello world

    // 注意:要可变借用,原变量本身必须声明 mut
    // let s = String::from("hi");
    // change(&mut s);    // 编译错误!s 不可变
}

fn append_world(s: &mut String) {
    s.push_str(" world");    // 修改借来的值
}

// 默认借用(&)是只读的,函数不能改
fn show(s: &String) {
    // s.push_str("x");   // 编译错误!不可变借用不能修改
    println!("{}", s);
}

几个细节:

3. 借用检查器的铁律

这是借用系统的核心规则,也是"无畏并发"的根源:

fn main() {
    let mut s = String::from("hello");

    // 借用检查器的铁律:
    // 同一时刻,要么有【多个不可变借用】,要么有【一个可变借用】
    // 两者不能同时存在

    // 情况一:多个不可变借用,OK(只读共享)
    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);
    // r1、r2 此后不再使用(NLL 优化)

    // 情况二:一个可变借用,OK
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);

    // 情况三(错误):同时存在可变与不可变借用
    // let r1 = &s;
    // let r2 = &mut s;     // 编译错误!
    // println!("{} {}", r1, r2);

    // 为什么这样设计?
    // 不可变借用承诺"不会变",可变借用会修改,二者冲突
    // 编译期阻止 -> 数据竞争从根源上无法发生(无畏并发!)
}

一句话总结:同一时刻,要么多个不可变借用(共享只读),要么一个可变借用(独占可写),两者不能共存。这条规则让数据竞争在编译期就被消灭——这是 Rust 区别于所有语言的核心价值。

新手会觉得它"太严格",但习惯后你会发现:它消灭了多线程编程里最痛的 bug 类型(运行时偶发的数据竞争)。代价是少数情况下要重构代码,但收益是绝对的安全。

4. NLL:更智能的生命周期

Rust 2018 之后引入了 NLL(Non-Lexical Lifetimes,非词法生命周期),让借用检查更智能——引用的作用域算到最后一次使用为止,而不是花括号结束:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;            // 不可变借用
    let r2 = &s;            // 又一个不可变借用
    println!("{} {}", r1, r2);
    // r1、r2 的"最后一次使用"在此结束

    // NLL(Non-Lexical Lifetimes,非词法生命周期):
    // Rust 2018 后,引用的作用域到【最后一次使用】为止
    // 而不是到【花括号结束】为止
    // 所以这里可以可变借用,不冲突!
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{}", r3);

    // 在 NLL 之前(2018 之前),这会编译错误
    // 因为 r1/r2 的生命周期被算到 main 结束
    // NLL 让借用检查器更智能,代码更自然
}

在 NLL 之前(2018 之前的 Rust),上面这段代码会编译错误,因为 r1/r2 的生命周期被机械地算到 main 结束。NLL 让编译器理解"r1、r2 在 println! 之后就没用了",从而允许后面的可变借用。这让现代 Rust 代码写起来自然得多。

5. 悬垂引用防护

Rust 永远不会出现悬垂引用(dangling reference)——引用指向已释放的内存。这是编译期保证的:

fn main() {
    // Rust 永远不会出现"悬垂引用"
    // 引用的生命周期不能超过它指向的数据

    // 这段代码编译错误:
    // fn dangle() -> &String {
    //     let s = String::from("hi");
    //     &s
    // }   // s 离开作用域被释放,引用指向无效内存!

    // Rust 编译器会拒绝,提示 "missing lifetime specifier"
    // 解决方案:直接返回 String(转移所有权给调用者)
    fn no_dangle() -> String {
        let s = String::from("hi");
        s      // 返回所有权,而不是引用
    }

    let s = no_dangle();
    println!("{}", s);   // hi
}

// 引用规则总结:
// 1. 任意时刻,要么多个 &,要么一个 &mut
// 2. 引用必须始终指向有效的数据(不能悬垂)

Rust 编译器会扫描所有引用的生命周期,确保它们不会比指向的数据活得更久。如果违反,直接编译错误。这是"内存安全"承诺的关键一环。

当函数返回引用时,编译器需要确认引用的目标在函数返回后仍然有效。复杂场景下你需要显式标注生命周期参数(如 &'a str),这是进阶话题,这篇先不展开。日常代码大多不需要手写生命周期标注,编译器自动推断。

6. 切片:特殊的引用

切片(slice)是一种特殊的引用,指向集合的连续一段。最常见的是字符串切片 &str:

fn main() {
    // 切片(slice)是一种特殊的引用,指向集合的连续一段
    // 字符串切片类型写作 &str

    let s = String::from("hello world");
    let hello: &str = &s[0..5];     // "hello"
    let world: &str = &s[6..11];    // "world"

    // 简写:从 0 开始可以省略,到末尾可以省略
    let hello = &s[..5];            // 同上
    let whole = &s[..];             // 整个字符串

    println!("{} {}", hello, world);

    // 字符串字面量的类型就是 &str
    // 它是"指向程序只读区"的切片,不可变
    let literal: &str = "我是一个字面量";

    // 数组也有切片
    let arr = [1, 2, 3, 4, 5];
    let slice: &[i32] = &arr[1..4];  // [2, 3, 4]
    println!("{:?}", slice);
}

// 字符串字面量 &str 和 String 的关系:
// String 是堆上可变的字符串(拥有所有权)
// &str 是字符串切片(借用,不可变)
// String 可以"降级"成 &str(&s),
// &str 也可以升级成 String(s.to_string() 或 s.to_owned())

几个要点:

7. 借用的最佳实践

8. 一个实战例子:函数同时返回值和引用

借用让函数可以"读"数据但还给原所有者。看一个常用模式——返回一个不拥有数据的切片:

9. 借用错误怎么读

遇到借用检查器报错时,关键是读懂错误信息。常见格式:

Rust 编译器的错误信息会画图指出每个引用的作用域,仔细看就能理解。多练几次,大脑会逐渐建立"借用直觉"。

小结

这一篇你掌握了 Rust 借用系统的全部要点:引用 &(只读借用)、可变引用 &mut(独占可写)、借用规则(多个 & 或一个 &mut,不可同时)、NLL(更智能的作用域)、悬垂引用防护切片(&str、&[T])。借用的铁律是"无畏并发"的根基——它让数据竞争在编译期就被消灭。

所有权 + 借用是 Rust 的全部灵魂。如果你这两篇读得有点晕,别担心——这是正常的"撞墙期"。下一篇字符串会通过具体场景巩固这些概念,你会看到 String 和 &str 在实际代码里如何配合所有权和借用。

← 上一篇 Rust 所有权(重点)

下一篇 Rust 字符串

✈️💬