Rust 所有权:move、copy、borrow 的规则
Rust 的所有权系统是其内存安全的核心机制,它在编译期通过一套严格的规则,确保程序不会出现悬垂指针、数据竞争等问题,而无需依赖垃圾回收。理解 move、copy 和 borrow 是掌握 Rust 的关键。
1. 理解所有权的基本原则
Rust 中的每个值都有一个“所有者”,并且同一时间只能有一个所有者。当所有者离开作用域时,该值会被自动释放。
记住三条核心规则:
- 每个值在任意时刻有且仅有一个所有者。
- 当所有者离开作用域,值被丢弃(drop)。
- 赋值或传参默认会转移所有权(move),而不是复制数据。
2. move:所有权的转移
当你将一个变量赋值给另一个变量,或者作为参数传递给函数,默认行为是 移动(move),即原变量不再有效。
执行以下操作:
let s1 = String::from("hello");
let s2 = s1; // s1 被 move 到 s2
// println!("{}", s1); // ❌ 编译错误:s1 已失效
String是堆分配类型,包含指针、长度和容量。- 如果允许
s1和s2同时拥有同一块堆内存,离开作用域时会重复释放,导致“双重释放”错误。 - Rust 通过 move 避免此问题:赋值后,
s1不再可用。
调用函数也会触发 move:
fn take_ownership(s: String) {
println!("{}", s);
} // s 在这里被 drop
let s = String::from("world");
take_ownership(s);
// println!("{}", s); // ❌ 错误:s 已被 move
3. copy:栈上数据的隐式复制
某些类型的数据只存在于栈上,复制成本极低,Rust 允许它们在赋值或传参时自动 复制(copy),而不是 move。
哪些类型实现了 Copy trait?
- 所有整数类型(如
i32、u64) - 布尔类型
bool - 浮点类型
f32、f64 - 字符
char - 元组(仅当所有元素都是
Copy类型时),如(i32, i32)
验证 copy 行为:
let x = 5;
let y = x; // x 是 Copy 类型,所以这里是复制
println!("x = {}, y = {}", x, y); // ✅ 正常运行
x和y是独立的栈上值,互不影响。- Rust 不允许你为已实现
Droptrait 的类型同时实现Copy,因为需要显式管理资源的类型不应被隐式复制。
4. borrow:临时借用而不获取所有权
有时你只想读取或修改数据,但不想拿走所有权。这时使用 借用(borrowing)。
4.1 不可变借用(&T)
创建一个不可变引用:
fn print_str(s: &String) {
println!("{}", s);
}
let s = String::from("hello");
print_str(&s); // 传入 &s,s 的所有权未被转移
println!("{}", s); // ✅ 依然可用
&s是对s的“借用”,函数参数s: &String表示“我只读,不拿走”。- 默认情况下,引用是不可变的。
4.2 可变借用(&mut T)
要修改借来的数据,必须使用可变引用:
fn push_str(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
push_str(&mut s);
println!("{}", s); // 输出 "hello, world"
- 原变量
s必须声明为mut。 - 调用时传入
&mut s。
5. 借用的核心限制规则
Rust 对引用施加两条硬性约束,防止数据竞争:
- 在任意时刻,要么有多个不可变引用(&T),要么有一个可变引用(&mut T),不能同时存在。
- 引用必须始终有效(不能悬垂)。
5.1 引用互斥规则示例
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// println!("{}, {}", r1, r2); // ✅ 允许多个不可变引用
let r3 = &mut s; // ❌ 编译错误!
// 不能在已有不可变引用时创建可变引用
正确做法:让不可变引用的作用域提前结束:
let mut s = String::from("hello");
{
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2);
} // r1, r2 在这里失效
let r3 = &mut s; // ✅ 现在可以创建可变引用
5.2 悬垂引用禁止
返回局部变量的引用会导致悬垂指针,Rust 禁止此行为:
fn dangle() -> &String {
let s = String::from("hello");
&s // ❌ 错误:s 在函数结束时被 drop,引用无效
}
编译器会报错:“s does not live long enough”。
解决方法:直接返回值(而非引用):
fn no_dangle() -> String {
let s = String::from("hello");
s // move 出去,所有权转移给调用者
}
6. 总结三种行为的适用场景
下表列出何时发生 move、copy 或 borrow:
| 操作 | 数据类型 | 发生的行为 |
|---|---|---|
赋值 let y = x; |
实现 Copy(如 i32) |
copy |
赋值 let y = x; |
未实现 Copy(如 String) |
move |
函数传参 f(x) |
Copy 类型 |
copy |
函数传参 f(x) |
非 Copy 类型 |
move |
函数传参 f(&x) |
任意类型 | borrow(不可变) |
函数传参 f(&mut x) |
mut x |
borrow(可变) |
7. 实践建议:如何写出符合所有权规则的代码
- 优先使用引用传递:除非你需要转移所有权,否则函数参数尽量用
&T或&mut T。 - 字符串字面量用
&str:函数参数若只读字符串,用&str而非&String,兼容性更好。fn greet(name: &str) { // 接受 &str println!("Hello, {}", name); } greet("Alice"); // 字符串字面量是 &str let s = String::from("Bob"); greet(&s); // &String 可自动转为 &str - 避免不必要的 clone:不要为了绕过 move 而随意调用
.clone(),这会带来运行时开销。先考虑是否能用引用。 - 利用作用域控制借用生命周期:用
{}显式结束引用的作用域,以便后续创建可变引用。
重构 move 为 borrow 的例子:
原始(低效):
fn calculate_len(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 必须返回 s,否则调用者无法再用
}
改进(推荐):
fn calculate_len(s: &String) -> usize {
s.len() // 不拿所有权,调用者继续使用
}
进一步优化(使用 &str):
fn calculate_len(s: &str) -> usize {
s.len()
}
暂无评论,快来抢沙发吧!