Rust: Rust 引用与借用:&T 和 &mut T 的完整指南
最后更新:2026-08-26
引用就像图书馆的阅览证——你可以借阅(只读引用),也可以申请唯一的改写权限(可变引用),但不能同时进行。
Rust 的引用(Reference)机制让你在不转移所有权的情况下访问数据。这就是"借用"(Borrowing)。
1. 你将学到
- 用
&T创建不可变引用(只读借用) - 用
&mut T创建可变引用(读写借用) - 借用规则:一个可变引用或多个不可变引用(不能同时存在)
- 引用的作用域与 NLL(Non-Lexical Lifetimes)
- 悬垂引用(Dangling Reference)与编译器的保护
- 引用在函数参数中的应用
2. 概念图解
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 编程》,它只能外借一次:
- Alice 借了这本书,所有权转移给了 Alice
- Bob 想看,但书已经不在图书馆了——所有权只属于一个人
- 图书馆必须重新买一本才行
"如果我能让多个人同时看同一本书,但不让他们撕毁它就好了……"
(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. 借用规则
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: 重要数据——已修改
关键点:不可变引用
r1和r2在最后一次使用后(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 可以,但需要标注生命周期参数。
📖 小节
- 引用(Reference) 让你不转移所有权就能访问数据——即"借用"
- 不可变引用
&T:允许多个同时存在,只读 - 可变引用
&mut T:一次只能有一个,读写 - 借用规则:多个
&T或一个&mut T,不能共存 - 引用必须始终有效,编译器防止悬垂引用
- NLL 让引用在最后一次使用后结束,而不是等到大括号结束
📝 作业
- 难度 ⭐:写一个函数
fn print_message(msg: &String),打印传入的字符串。在main中创建String后调用此函数,验证调用后原变量仍然可用。 - 难度 ⭐⭐:写一个程序,创建一个
mut String,先创建两个不可变引用并打印,然后创建一个可变引用修改内容,再打印修改后的结果。 - 难度 ⭐⭐⭐:尝试写一个返回引用的函数(如
fn get_ref() -> &String),观察编译器的报错信息。然后修改为返回String所有权,理解为什么 Rust 禁止返回局部变量的引用。