所有权与借用入门
1. 这是什么
所有权(ownership)是 Rust 最核心的语言机制之一。
它决定一段数据在某个时刻“归谁负责”,以及这段数据什么时候可以被读取、修改、转移和释放。
借用(borrowing)则是在不拿走所有权的前提下,临时去访问这段数据的方式。
一句话理解:
- 所有权回答的是:这份值现在归谁管
- 借用回答的是:我能不能先看一下,或者暂时改一下
Rust 之所以能在没有 GC 的情况下仍然保证内存安全,很大程度上就是因为它把这些规则放到了编译期检查。
2. 为什么重要
很多语言里,初学者写代码时很少主动思考“值的归属”。
但在 Rust 里,如果不理解所有权,后面的移动、借用、生命周期、trait 设计几乎都会觉得别扭。
理解这个机制后,你会明白:
- 为什么一个值赋给另一个变量后,旧变量可能不能再用
- 为什么同一时刻不能同时存在“可变借用”和“不可变借用”冲突
- 为什么 Rust 能在编译期拦住很多悬垂引用和数据竞争问题
所以它不是一个孤立知识点,而是整门语言的底层规则。
3. 先建立直觉
先看一个最简单的例子:
fn main() {
let s = String::from("hello");
let t = s;
println!("{}", t);
}这段代码里:
s原本拥有这段字符串数据let t = s;之后,所有权被移动给了t- 此时
s不再有效
如果你再写:
println!("{}", s);编译器就会报错。
这说明 Rust 不是简单地“复制一下变量名”,而是非常认真地跟踪值的归属。
可以把它想象成:
- 一个箱子里装着资源
- 某一时刻只能有一个人负责这个箱子
- 你可以把箱子交给别人
- 也可以让别人暂时看看里面有什么
- 但规则必须明确,不能同时乱抢
4. 核心内容
4.1 所有权三条基本规则
理解所有权,先记住三条核心规则:
- Rust 中每个值都有一个 owner
- 同一时刻一个值只能有一个 owner
- owner 离开作用域时,值会被自动释放
例如:
fn main() {
{
let s = String::from("hello");
println!("{}", s);
}
// 这里 s 已经离开作用域,内存会被自动回收
}这就是 Rust 不依赖 GC 也能安全管理资源的基础。
4.2 Copy 与 Move
不是所有赋值都会触发“所有权转移”的那种紧张感。
Rust 会区分 Copy 类型 和 Move 类型。
Copy 类型
像整数、布尔值、字符这类小而简单的值,通常会直接复制:
fn main() {
let x = 10;
let y = x;
println!("{}", x);
println!("{}", y);
}这里 x 还能继续使用,因为 i32 是 Copy 类型。
Move 类型
像 String、Vec<T> 这类拥有堆内存资源的类型,默认更常见的是 move:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);
}这里 s1 的所有权已经转移给 s2。
4.3 克隆不是移动
如果你确实想保留两份独立数据,可以显式调用 clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);
println!("{}", s2);
}这时:
s1还有效s2是一份新的拷贝- 代价也比简单复制更高,因为它可能涉及堆内存分配
所以要建立一个意识:
=不等于深拷贝- 需要复制真实数据时要显式说出来
4.4 借用:不拿走所有权地访问值
如果只是想读某个值,通常不需要拿走它的所有权,而是借用:
fn print_len(s: &String) {
println!("len = {}", s.len());
}
fn main() {
let s = String::from("hello");
print_len(&s);
println!("{}", s);
}这里:
&s表示对s的不可变借用print_len只是“看一下”这段字符串- 所有权仍然在
main里的s手上
这是写 Rust 时最常见的模式之一。
4.5 可变借用
如果你想修改值,就要用可变借用:
fn append_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s);
}这里需要同时满足两件事:
- 变量本身要是
mut - 借用时也要写
&mut
4.6 借用规则为什么这么严格
Rust 最关键的借用规则之一是:
- 同一时刻可以有多个不可变借用
- 或者只能有一个可变借用
- 但不能两种同时冲突存在
例如:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
r3.push_str("!");
}这段是可以的,因为不可变借用 r1、r2 在使用完之后,后面才出现可变借用 r3。
但下面这种通常就不行:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{} {}", r1, r2);
}因为这意味着:
- 一边有人在只读观察这份数据
- 另一边又有人准备修改它
编译器会阻止这种潜在冲突。
4.7 函数参数里的所有权
函数调用时也经常发生所有权移动:
fn takes_string(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
takes_string(s);
}这里 takes_string 拿走了 s 的所有权。调用后,main 里的 s 就不能再用了。
如果只是想读取它,更常见写法是:
fn takes_str(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
takes_str(&s);
println!("{}", s);
}这也是为什么 Rust 代码里经常能看到 &str、&T、&mut T 这样的参数形式。
5. 常见误区
5.1 误区一:所有权就是语法限制
不是。
它本质上是在用编译期规则换运行期安全。
你表面上觉得是“语法限制很多”,但底层得到的是:
- 更明确的资源边界
- 更少的悬垂引用
- 更少的数据竞争
- 更可预测的性能
5.2 误区二:能 clone 就一直 clone
clone() 很有用,但不能把它当成“编译不过就暴力绕过去”的万能按钮。
如果一遇到借用报错就 clone:
- 代码可能掩盖真实数据流
- 性能可能变差
- 你会始终学不会所有权模型
更好的做法是先问:
- 这里真的需要复制数据吗
- 能不能借用
- 能不能调整函数签名
5.3 误区三:可变借用越多越灵活
Rust 不是鼓励“到处改状态”,而是鼓励你明确状态变化发生在哪里。
可变借用越多,数据流越难推理。
很多时候,先用不可变借用思考问题,代码反而更清晰。
6. 一个更实用的判断思路
写 Rust 时如果卡住了,可以按这几个问题排查:
- 这个值现在到底归谁所有
- 我这里只是读取,还是要修改
- 我需要拿走所有权,还是借用就够了
- 这里是不是不小心让不可变借用和可变借用重叠了
- 我加
clone()是真的需要,还是只是为了绕过错误
很多初学者报错看不懂,其实最后都能归到这几类问题上。
7. 学习建议
学习所有权时,不要只背规则,要反复做这种转换练习:
- 先写一个会 move 的版本
- 再改成借用版本
- 再改成可变借用版本
- 再观察哪些地方需要
String,哪些地方只需要&str
只要这种转换做多了,脑子里就会慢慢形成“值的流动图”。
8. 自测标准
- 能解释 owner、move、borrow 分别是什么意思
- 能区分 Copy 类型和会 move 的类型
- 能解释为什么
String赋值后原变量常常失效 - 能写出不可变借用和可变借用的基本函数签名
- 遇到借用错误时,能先从“值归谁”和“借用是否冲突”开始排查