Rust: Rust 引用与借用:&T 和 &mut T 的完整指南

最后更新:2026-08-26

引用就像图书馆的阅览证——你可以借阅(只读引用),也可以申请唯一的改写权限(可变引用),但不能同时进行。

Rust 的引用(Reference)机制让你在不转移所有权的情况下访问数据。这就是"借用"(Borrowing)。


1. 你将学到


2. 概念图解

100%
flowchart LR
    subgraph "所有权传递"
        A1["数据 String"] -->|"let s2 = s1<br>move"| B1["新所有者 s2"]
        B1 -->|"s1 失效"| C1["编译错误<br>println!(s1)"]
    end
    subgraph "引用借用"
        A2["数据 String"] -->|"let r = &s<br>borrow"| B2["引用 r"]
        B2 -->|"r 使用后归还<br>s 仍然有效"| C2["✅ 安全访问"]
    end

3. 一个图书馆的故事

(1) 痛苦:书被撕坏了

Tom 的图书馆里有一本热门书《Rust 编程》,它只能外借一次:

"如果我能让多个人同时看同一本书,但不让他们撕毁它就好了……"

(2) Rust 引用的解决方案

RUST
fn main() {
    let book = String::from("Rust 编程");

    // 多个不可变引用:多人同时"只读"借阅
    let reader1 = &book;   // 读者 1 借阅(只读)
    let reader2 = &book;   // 读者 2 借阅(只读)
    println!("读者 1 看到: {}", reader1);
    println!("读者 2 看到: {}", reader2);
    // 两个引用同时有效——因为大家都是只读

    let mut notebook = String::from("笔记本");

    // 可变引用:唯一改写权限
    let writer = &mut notebook;  // 只有一个人可以改写
    writer.push_str("——写了一些笔记");
    println!("改写者: {}", writer);
    // writer 在这里结束作用域,其他人才能再借
}

引用的规则就像图书馆的规则:要么允许多人同时阅读(多个 &T),要么允许一个人独自修改(一个 &mut T),不能同时发生。


4. 借用规则

100%
graph TB
    A[借用规则] --> B[你只能选择其一]
    B --> C[多个不可变引用 &T]
    B --> D[或者一个可变引用 &mut T]
    B --> E[不能同时存在]
    A --> F[引用必须始终有效]
    F --> G[不能有悬垂引用]
规则 说明 违反后果
多个 &T 或一个 &mut T 不能同时有不可变引用和可变引用 编译错误
引用必须有效 引用的对象不能比引用活得更久 编译错误
引用不可变时,原值也不能变 有 &T 存在时,原值不能通过 &mut T 修改 编译错误
NLL 作用域 &mut T 的作用域在最后一次使用后结束 编译器智能优化

(2) 引用类型对比

引用类型 语法 权限 可同时存在数量 典型用途
不可变引用 &T 只读 多个 函数参数只读访问
可变引用 &mut T 读写 1 个 函数参数修改数据
胖指针(切片) &[T] 只读 多个 数组/字符串切片

(3) 借用场景速查

场景 推荐方式 示例
函数只读访问 &T fn len(s: &String) -> usize
函数修改数据 &mut T fn push(s: &mut String, ch: char)
函数消费数据 T(传值) fn consume(s: String)
函数返回数据 T(返回值) fn create() -> String
多函数共享只读 &T 多个函数接收同一 &T
交替修改 &mut T(分时复用) NLL 允许交替创建 &mut T

5. 引用示例

▶ 示例 1:不可变引用——只读访问(难度 ⭐⭐)

RUST
// ============================================
// 不可变引用 &T:多个读者可以同时借阅
// ============================================

fn calculate_length(s: &String) -> usize {
    // s 是 String 的引用,没有所有权
    s.len()
}  // s 离开作用域,但因为是引用,不会销毁 String

fn main() {
    let s = String::from("Hello, Rust!");

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

    // 多个不可变引用可以共存
    let r1 = &s;
    let r2 = &s;
    println!("r1: {}, r2: {}", r1, r2);  // ✅ 同时读取
}

输出:

TEXT 📖 仅展示
'Hello, Rust!' 的长度是: 12
r1: Hello, Rust!, r2: Hello, Rust!

&s 创建对 s 的引用,传递给函数时不会转移所有权。函数结束后,引用失效,但 s 仍然存在。这就是"借用"——用完了还回来。


▶ 示例 2:可变引用——唯一修改权限(难度 ⭐⭐)

RUST
// ============================================
// 可变引用 &mut T:一次只能有一个写者
// ============================================

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

    // 创建一个可变引用
    let r = &mut s;
    r.push_str(", world!");
    println!("通过可变引用修改: {}", r);
    // r 在这里最后一次使用,NLL 结束

    // 可变引用结束后,可以再创建新的可变引用
    let r2 = &mut s;
    r2.push_str("!!");
    println!("另一个可变引用: {}", r2);

    // --- 错误示例(取消注释看编译错误)---
    // let mut s2 = String::from("test");
    // let ref1 = &mut s2;
    // let ref2 = &mut s2;  // ❌ 不能同时有两个可变引用
    // println!("{}, {}", ref1, ref2);
}

输出:

TEXT 📖 仅展示
通过可变引用修改: Hello, world!
另一个可变引用: Hello, world!!!

&mut 是可变的——可以修改引用的值。但 Rust 强制一次只能有一个可变引用。这防止了数据竞争——不需要锁,不需要原子操作,完全在编译时解决。


▶ 示例 3:不可变引用和可变引用不能共存(难度 ⭐⭐⭐)

RUST
// ============================================
// 演示不可变引用和可变引用的冲突
// ============================================

fn main() {
    let mut data = String::from("重要数据");

    let r1 = &data;       // ✅ 不可变引用 1
    let r2 = &data;       // ✅ 不可变引用 2
    println!("r1: {}, r2: {}", r1, r2);
    // r1 和 r2 在这里最后一次使用

    let r3 = &mut data;   // ✅ 此时可以创建可变引用
    r3.push_str("——已修改");
    println!("r3: {}", r3);

    // --- 错误示例:不可变引用还在时不能创建可变引用 ---
    // let mut s = String::from("演示");
    // let r_a = &s;        // 不可变引用
    // let r_b = &mut s;    // ❌ 编译错误:已经有不可变引用
    // println!("{}, {}", r_a, r_b);
}

输出:

TEXT 📖 仅展示
r1: 重要数据, r2: 重要数据
r3: 重要数据——已修改

关键点:不可变引用 r1r2 在最后一次使用后(println!)结束作用域,然后可变引用 r3 才能创建。这就是 NLL(Non-Lexical Lifetime)——编译器能智能判断引用何时不再使用,而不是等大括号结束。


▶ 示例 4:悬垂引用预防(难度 ⭐⭐⭐)

RUST
// ============================================
// 编译器如何防止悬垂引用
// ============================================

// 错误示例:返回局部变量的引用
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s  // ❌ 编译错误:s 在函数结束时被销毁,引用将悬垂
// }

// 正确的做法:直接返回 String(转移所有权)
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 直接返回 String,所有权转移给调用者
}

fn main() {
    let s = no_dangle();
    println!("安全的返回值: {}", s);

    // 引用局部变量的正确方式:在作用域内使用
    let local = String::from("局部数据");
    let r = &local;          // 引用
    println!("引用局部变量: {}", r);
    // r 在这里结束,local 仍然有效
}  // local 在这里被销毁(晚于 r),安全

输出:

TEXT 📖 仅展示
安全的返回值: hello
引用局部变量: 局部数据

悬垂引用(Dangling Reference)是指引用指向的内存已经被释放。Rust 编译器在编译时就能检测到这种情况——如果引用指向的对象在引用之后被销毁,编译器会报错。这完全消除了"悬垂指针"这类 bug。


▶ 示例 5:综合练习——银行账户操作(难度 ⭐⭐⭐)

RUST
// ============================================
// 综合示例:引用在真实场景中的运用
// ============================================

struct Account {
    owner: String,
    balance: f64,
}

fn check_balance(account: &Account) -> String {
    format!("{} 的余额: {:.2} 元", account.owner, account.balance)
}

fn deposit(account: &mut Account, amount: f64) {
    if amount <= 0.0 {
        println!("存款金额必须大于 0");
        return;
    }
    account.balance += amount;
    println!("存入 {:.2} 元成功", amount);
}

fn transfer(from: &mut Account, to: &mut Account, amount: f64) -> bool {
    if amount <= 0.0 || from.balance < amount {
        println!("转账失败:余额不足或金额无效");
        return false;
    }
    from.balance -= amount;
    to.balance += amount;
    println!("转账 {:.2} 元: {} → {}", amount, from.owner, to.owner);
    true
}

fn show_accounts(accounts: &[Account]) {
    println!("--- 账户列表 ---");
    for (i, acc) in accounts.iter().enumerate() {
        println!("{}. {} 余额: {:.2} 元", i + 1, acc.owner, acc.balance);
    }
}

fn main() {
    let mut alice = Account { owner: String::from("Alice"), balance: 1000.0 };
    let mut bob = Account { owner: String::from("Bob"), balance: 500.0 };

    println!("{}", check_balance(&alice));
    println!("{}", check_balance(&bob));

    deposit(&mut alice, 200.0);
    println!("{}", check_balance(&alice));

    transfer(&mut alice, &mut bob, 300.0)?;

    let accounts = [&alice, &bob];
    show_accounts(&accounts);

    let total: f64 = accounts.iter().map(|a| a.balance).sum();
    println!("总存款: {:.2} 元", total);
}

输出:

TEXT 📖 仅展示
Alice 的余额: 1000.00 元
Bob 的余额: 500.00 元
存入 200.00 元成功
Alice 的余额: 1200.00 元
转账 300.00 元: Alice → Bob
--- 账户列表 ---
1. Alice 余额: 900.00 元
2. Bob 余额: 800.00 元
总存款: 1700.00 元

此示例展示了 &T(只读查询)、&mut T(修改操作)和 &[T](只读遍历)三种引用方式的实际应用。transfer 同时需要两个 &mut T,这在 Rust 中是安全的因为它们指向不同数据。


❓ 常见问题

Q 引用和指针有什么区别?
A 引用是保证有效的指针。
Q &T 和 &mut T 能同时存在吗?
A 不能。
Q 什么是 NLL(Non-Lexical Lifetime)?
A NLL 让引用在最后一次使用后结束,而不是等到大括号结束。
Q 什么时候用引用而不是传所有权?
A 当你只需要"看一下"数据时用引用,当你需要"拥有"或"长期持有"数据时用所有权。
Q 引用能存储到结构体里吗?
A 可以,但需要标注生命周期参数。

📖 小节


📝 作业

  1. 难度 ⭐:写一个函数 fn print_message(msg: &String),打印传入的字符串。在 main 中创建 String 后调用此函数,验证调用后原变量仍然可用。
  2. 难度 ⭐⭐:写一个程序,创建一个 mut String,先创建两个不可变引用并打印,然后创建一个可变引用修改内容,再打印修改后的结果。
  3. 难度 ⭐⭐⭐:尝试写一个返回引用的函数(如 fn get_ref() -> &String),观察编译器的报错信息。然后修改为返回 String 所有权,理解为什么 Rust 禁止返回局部变量的引用。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏