引用和借用
约 2780 字大约 9 分钟
2026-02-05
关键词:所有权(ownership)、移动(move)、引用(reference)、借用(borrowing)、可变引用(mutable reference)、悬垂引用(dangling reference)、生命周期(lifetime)
1) 背景引入:为什么“把 String 传进函数”会出问题?
在 Rust 里,String 这类“拥有堆内存”的类型,默认遵循 所有权(ownership) 规则:
- 谁拥有(own)数据,谁负责在离开作用域时释放它。
- 当你把一个值传给函数,如果参数类型是
String(不是引用),通常会发生 移动(move): 调用者把所有权交给被调用函数。
这会带来一个非常典型的“初学者困惑”:
我只是想让函数算一下长度,为什么算完我就不能再用这个
String了?
原因是:你把 String 移进函数里了,调用结束后它已经不再属于你。
传统“补救方案”的缺点:用元组把 String 再还回来
很多语言里会复制或隐式共享;但 Rust 不这么做。为了继续使用那个 String,你可能不得不让函数把 String 连同结果一起返回(比如返回 (String, usize))。
问题是:太啰嗦,而且只是为了“看一下数据”,却搞得像“转交产权再归还”。
2) 核心原理解析:引用是什么?借用是什么?它解决了什么历史难题?
2.1 引用(reference)是什么?
引用(reference)可以理解为:
- 它像指针(pointer)一样保存一个地址:你可以“顺藤摸瓜”访问那块数据。
- 但它和传统指针有关键区别: Rust 保证引用在其生命周期内指向有效值(不会指向已经释放的内存)。
也就是说,你可以“看/用”数据,但不必拥有它。
这就是借用(borrowing):我用一下你的东西,但我不拿走所有权,用完还给你。
2.2 为什么这很重要?
这是 Rust 设计上非常核心的目标: 在不依赖 GC(垃圾回收)的情况下,尽可能在编译期消灭内存安全问题。
在 C/C++ 这类语言里,你常见两类灾难:
- 悬垂指针(dangling pointer):指针还在,但内存已被释放或复用。
- 数据竞态(data race):多个线程/指针同时读写同一数据,没有同步导致未定义行为。
Rust 用“所有权 + 借用规则”在编译期把这两类问题直接堵死。
3) 实操步骤:从 &String 到 &mut String,再到借用规则
3.1 用引用传参:只读借用(immutable borrowing)
我们先实现“计算长度但不拿走 String”的版本:
fn main() {
// s1 拥有这段堆内存(String 的内容在堆上)
let s1 = String::from("hello");
// 传入 &s1:创建一个对 s1 的引用(只读借用)
// 注意:这里没有把 s1 “移动”进去
let len = calculate_length(&s1);
// 依然可以使用 s1,因为它的所有权从未被转移
println!("The length of '{s1}' is {len}.");
}
// 参数类型是 &String:表示“我只借来看看”
fn calculate_length(s: &String) -> usize {
// s.len() 只读访问,不改变 String
s.len()
// s 在这里离开作用域,但它只是引用:不会触发 String 的释放
}这一段代码背后发生了什么?
s1仍然是数据的拥有者。calculate_length得到的是s1的引用:可以读,但不负责释放。
下面用一个“内存关系图”把它讲清楚:

你可以把它理解成:
String本体(指针/长度/容量)在栈上- 真正的字符内容在堆上
&String只是指向某个String的“借条”
3.2 反向概念:解引用(dereferencing, *)
引用的“反义动作”是解引用(dereferencing),通过 * 运算符完成。
本章你暂时不需要深入,只要记住:
&是“拿到引用”,*是“从引用拿到值”。
3.3 试图修改借来的东西:为什么不让?
来看一个“必炸”的例子:
fn main() {
let s = String::from("hello");
// ❌ change 只接收 &String(只读借用)
change(&s);
}
fn change(some_string: &String) {
// ❌ 报错:不能通过只读引用修改数据
some_string.push_str(", world");
}这背后的逻辑非常朴素:
你拿的是“只读借条”,凭什么改人家的东西?
所以 Rust 的规则是:
- 引用默认也是不可变的(immutable)。
&T只允许读,不允许写。
3.4 想修改怎么办?可变引用(mutable reference, &mut T)
要修改,就得明确表达“我要借来修改”:
fn main() {
// ✅ 变量本身必须是可变的
let mut s = String::from("hello");
// ✅ 借用时用 &mut:创建可变引用
change(&mut s);
println!("{s}"); // hello, world
}
// ✅ 参数类型是 &mut String:表示“我借来并会修改它”
fn change(some_string: &mut String) {
some_string.push_str(", world");
}这里有两个“必须同时满足”的点:
- 被借用者得是
mut(let mut s = ...) - 借用方式得是
&mut(change(&mut s))
这让代码读起来非常诚实:
“这个函数会改变传入的值。”
4) Rust 借用的关键规则:为什么它能防数据竞态?
Rust 对可变引用有一条很“严格但值钱”的限制:
同一时刻,对同一份数据:只能存在一个可变引用(&mut)。
4.1 两个可变引用会怎样?直接拒编译
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ 同时第二次可变借用:不允许
println!("{r1}, {r2}");
}编译器在担心什么?
如果允许多个 &mut 同时存在,那么就可能出现:
- 两个“写入口”同时改同一份数据
- 写入顺序不确定
- 读到中间状态
- 线程下更是直接形成数据竞态
Rust 的做法是:把这种风险在编译期就掐断。
用图表示“为什么不行”:

4.2 想要多个可变引用?可以,但不能“同时”
Rust 允许你“分时复用”,用作用域({})切开即可:
fn main() {
let mut s = String::from("hello");
{
// ✅ r1 在这个小作用域内独占可变借用
let r1 = &mut s;
r1.push_str("!");
} // ✅ r1 结束,借用释放
// ✅ 现在才可以创建新的可变引用 r2
let r2 = &mut s;
r2.push_str("?");
}4.3 可变引用和不可变引用能混用吗?
规则同样直接:
同一时刻:要么多个只读(
&T),要么一个可写(&mut T)。
下面这个会报错:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ 只读借用
let r2 = &s; // ✅ 只读借用
let r3 = &mut s; // ❌ 同时存在只读借用时,不允许可写借用
println!("{r1}, {r2}, and {r3}");
}原因也很直觉:
- 读的人会默认数据“不会突然变”
- 如果你允许同时存在
&mut,读者看到的值可能在读的过程中变掉
4.4 “引用的作用域”不是看花括号,而是看最后一次使用
这一点非常关键,很多人第一次会误解:
引用从创建开始,一直持续到它最后一次被使用的位置。
所以这段是可以编译的:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// ✅ r1/r2 最后一次使用在这里
println!("{r1} and {r2}");
// ✅ 在 r1/r2 不再使用之后,才创建可变引用:OK
let r3 = &mut s;
println!("{r3}");
}Rust 编译器能判断“引用后面还用不用”,从而放宽限制——这也是 Rust 借用检查器强大的地方。
5) 悬垂引用:Rust 为什么死也不让你返回 &String?
在指针语言里,这类 bug 很致命:
- 你返回一个指向局部变量的指针
- 函数结束局部变量被释放
- 指针还指着那块已经无效的内存 => 悬垂指针(dangling pointer),运行时可能崩溃或产生诡异数据
Rust 直接在编译期阻止这种事情发生。
看这个例子:
fn main() {
let reference_to_nothing = dangle();
}
// ❌ 试图返回一个引用
fn dangle() -> &String {
let s = String::from("hello");
&s // ❌ 返回对局部变量 s 的引用
} // s 在这里被释放,引用将指向无效内存 -> Rust 不允许你可以用一句人话理解编译器的核心抱怨:
“你返回的是借来的东西,但你没告诉我它到底是从谁那借来的;而且借主马上就要消失了。”
这会牵扯到一个重要概念:生命周期(lifetime) (原文也说会在后续章节展开)。
正确做法:直接返回拥有权(return owned value)
解决方案非常简单:
fn no_dangle() -> String {
let s = String::from("hello");
// ✅ 直接把所有权移动出去:函数结束不会释放它
s
}这一步背后的思想是:
- 如果你想让数据在函数外继续活着 就把所有权交给调用者
- 不要试图让调用者拿着“指向已死亡对象的借条”
6) 扩展思考:把这些规则串起来,你就理解了 Rust 的“安全哲学”
6.1 为什么 Rust 这么“烦”?
因为它把两类运行时灾难,提前到编译期解决:
- 内存悬垂/野指针问题 → 通过生命周期与借用规则消灭
- 数据竞态 → 通过“同一时刻唯一可写入口”消灭
6.2 你写 Rust 时真正该培养的直觉
你每次报借用错误时,都可以问自己三句话:
- 谁拥有这份数据?(owner 是谁)
- 我现在是想读它还是改它?(& 还是 &mut)
- 在同一时刻,还有没有其他人也在拿着它?(是否重叠借用)
6. 3 题目测试

let x = Box::new(0); // x: Box<i32>
let y = Box::new(&x); // y: Box<&Box<i32>>也就是说:
x 是一个
Box<i32>(Box 指向堆上的 0)&x是一个&Box<i32>(引用指向栈上的 x)y 是一个
Box<&Box<i32>>(Box 里面装着“&x”这个引用)
为什么是 3 次解引用? 我们从 y 出发“追指针”到 0,要跨 3 条“箭头”:
y:
Box<&Box<i32>>解引用 1 次:*y得到里面装的东西 →&Box<i32>现在是
&Box<i32>解引用 2 次:*(*y)得到 →Box<i32>(也就是 x)现在是
Box<i32>解引用 3 次:*(*(*y))得到 → i32,也就是 0
所以从“层级结构/指向关系”上看,确实是:
y → &x → x → 0 (3 次)
用 ASCII 画一下更直观:
y : Box< &Box<i32> >
|
v (第1次解引用:Box -> 里面的 &Box)
&x : &Box<i32>
|
v (第2次解引用:& -> x)
x : Box<i32>
|
v (第3次解引用:Box -> 0)
0 : i32但你可能会遇到一个“坑”:***y 在 Rust 里不一定能直接写出来 因为第 2 次解引用会尝试把 x: Box<i32> 从 &x 里“搬走”(move out of borrowed content),这在 Rust 里通常是不允许的。
更“Rust”的写法:别让 y 存 &Box<i32>,而是存 &i32 你想拿到 0,其实只需要借到 i32 的引用就够了:
let x = Box::new(0); // x: Box<i32>
let y = Box::new(x.as_ref()); // y: Box<&i32>
// 现在 y: Box<&i32>
// 取 0 只需要两次:*y 得到 &i32,再 * 得到 i32
let v = **y;一句话总结 题目答案“3 次”讲的是指向层级:Box → 引用 → Box → 值。
贡献者
flycodeu
版权所有
版权归属:flycodeu
