Rust入门:所有权系统理解

Rust 的所有权系统是它区别于其他语言的核心特性,也是新手最大的障碍。本文用具体代码解释所有权、借用和生命周期的核心规则。

所有权三规则

Rust 编译器在编译期强制执行以下规则:

  1. 每个值有且仅有一个所有者(owner)
  2. 同一时刻,要么有一个可变引用,要么有任意多个不可变引用
  3. 所有者离开作用域时,值被自动释放(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。习惯之后,你会发现这套系统让你写出更清晰的数据流代码。