Rust 的所有权系统是它区别于其他语言的核心特性,也是新手最大的障碍。本文用具体代码解释所有权、借用和生命周期的核心规则。
所有权三规则
Rust 编译器在编译期强制执行以下规则:
- 每个值有且仅有一个所有者(owner)
- 同一时刻,要么有一个可变引用,要么有任意多个不可变引用
- 所有者离开作用域时,值被自动释放(drop)
这三条规则让 Rust 在没有垃圾回收的情况下保证内存安全。
移动语义(Move)
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权转移给 s2
// println!("{}", s1); // 编译错误!s1 已经无效
println!("{}", s2); // OK
}
赋值 let s2 = s1 不是拷贝数据,而是把 s1 的所有权移动到 s2。之后 s1 变成无效状态。这和 C++ 的移动语义类似,但 Rust 在编译期强制检查。
对于实现了 Copy trait 的类型(如 i32、f64、bool),赋值是拷贝而不是移动:
let x = 42;
let y = x; // i32 实现了 Copy,这里是拷贝
println!("{}", x); // OK,x 仍然有效
克隆(Clone)
如果确实需要深拷贝堆上的数据,显式调用 clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝
println!("s1={}, s2={}", s1, s2); // 都有效
clone() 有性能开销,Rust 要求你显式写出来,让代价可见。
函数传参与返回值
函数传参同样遵循移动/拷贝语义:
fn take_ownership(s: String) {
println!("{}", s);
} // s 在这里被 drop
fn main() {
let s = String::from("hello");
take_ownership(s);
// println!("{}", s); // 编译错误,所有权已转移
}
返回值可以把所有权转移回来:
fn give_back(s: String) -> String {
s // 所有权转移给调用者
}
但每次都转移所有权太麻烦,这就是借用存在的意义。
借用(Borrowing)
不可变借用 &T,允许读取但不允许修改:
fn calc_length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let len = calc_length(&s); // 借用,不转移所有权
println!("'{}' 的长度是 {}", s, len); // s 仍然有效
}
可变借用 &mut T,允许修改但同一时刻只能有一个:
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello, world"
}
编译器禁止同时存在多个可变借用:
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 编译错误!不能同时有两个可变借用
println!("{}", r1);
也不能在有不可变借用时创建可变借用:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s; // OK,多个不可变借用
// let r3 = &mut s; // 编译错误!r1、r2 还在使用
println!("{}, {}", r1, r2);
// r1、r2 在这里之后不再使用,后面可以创建可变借用
let r3 = &mut s; // OK(NLL:Non-Lexical Lifetimes)
println!("{}", r3);
生命周期初探
当函数返回引用时,编译器需要知道返回的引用和哪个输入参数的生命周期绑定:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("longer: {}", result); // OK,s2 还活着
}
// println!("{}", result); // 如果放这里会编译错误,s2 已被 drop
}
'a 是生命周期标注,告诉编译器:返回值的生命周期是 x 和 y 中较短的那个。这不是改变生命周期,而是给编译器提供约束信息。
小结
所有权系统的本质是在编译期追踪每个值的生命周期和访问权限。刚开始写 Rust 会频繁和编译器"吵架",但每次编译错误都在阻止一个潜在的内存 bug。习惯之后,你会发现这套系统让你写出更清晰的数据流代码。