讲解

所有权是 Rust 最独特、也最需要花时间消化的机制。规则只有三条:一、每个值都有一个所有者(owner);二、同一时刻只能有一个所有者;三、所有者离开作用域,值被自动释放(drop)。没有垃圾回收器,也没有手动 free——内存释放在编译期就被精确安排好了。

关键在「赋值即移动」(move)。对 String 这种持有堆内存的类型,let s2 = s1 不是复制内容,而是把堆数据的所有权从 s1 转移给 s2,s1 立刻失效——之后再用 s1 就是编译错误。这么做的理由:如果两个变量都认为自己是所有者,离开作用域时同一块内存会被释放两次(double free),Rust 用「移动后旧变量失效」在编译期根除了这个问题。想要真正的深拷贝,显式调用 .clone()——它有堆分配开销,所以必须显式写。

反过来,i32、bool、char、f64 这类完全存在栈上的固定大小类型实现了 Copy 特征,赋值时是复制而不是移动,旧变量照常可用。这就是为什么 let y = x 对数字不报错、对 String 报错。函数传参和返回值遵循同样的规则:把 String 传给函数就是把所有权交出去(函数返回可以再拿回来);传 i32 则是复制。

示例

移动、失效与显式 clone:

fn main() {
    let s1 = String::from("所有权");
    let s2 = s1; // 所有权从 s1 移动到 s2,s1 从此失效
    println!("s2 = {s2}");

    let a = String::from("原数据");
    let b = a.clone(); // 深拷贝:a 和 b 各自拥有一份
    println!("a = {a}, b = {b}");

    assert_eq!(s2, "所有权");
    assert_eq!(a, b);
}

如果在移动之后继续使用旧变量,编译器会拒绝(构建脚本会验证这段确实编译失败):

// compile-fail: 移动后使用旧变量
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}");
    println!("{s2}");
}

报错大意(E0382,借用了一个已移动的值):

error[E0382]: borrow of moved value: `s1`
 --> main.rs:4:20
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`,
  |            which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}");
  |               ^^^^ value borrowed here after move

Copy 类型的赋值是复制;函数传参会移动所有权:

fn takes_ownership(s: String) -> usize {
    s.len()
} // s 在这里离开作用域并被释放

fn makes_copy(n: i32) -> i32 {
    n * 2
}

fn main() {
    let x = 5;
    let doubled = makes_copy(x); // i32 是 Copy,x 仍然有效
    assert_eq!(x, 5);
    assert_eq!(doubled, 10);

    let text = String::from("交出所有权");
    let len = takes_ownership(text);
    // text 在这里已经失效,不能再使用
    println!("长度是 {len}");
    assert_eq!(len, 15); // 5 个汉字 × 3 字节
}

常见坑

  • 以为 = 总是复制:对 String、Vec 这类堆类型,= 是移动;对数字、bool、char 是复制。判断标准:类型是否实现了 Copy。
  • 到处 clone 求太平:clone 是真实的堆分配开销,大量滥用会拖慢程序。多数「不得不 clone」的场景其实该用引用(下一章)。
  • 把 &str 字面量当 String:"hello" 是 &str(指向程序二进制的字符串切片),String 是拥有堆内存的类型;两者方法集不同,转换用 String::from 或 .to_string()。
  • 传参后还想用原值:函数拿走所有权后就别再用了——要么让函数返回回来,要么(更好的方案)传引用。

小结

三规则:一个值一个所有者、所有者离开即释放、赋值即移动(Copy 类型除外)。移动语义杜绝了二次释放,也让所有权流向清晰可读。但每次都交出所有权太笨拙——下一章的引用与借用才是日常写法。