所有权、借用和生命周期 beginner 30 分钟 更新 2026-06-15

所有权与移动

理解 Rust 的所有权规则、移动语义和 clone 的代价。

所有权与移动

你会学到什么

  • 每个值都有唯一的所有者,所有者离开作用域时值被自动释放。
  • 把值传给函数或赋给新变量会发生“移动”,原变量随后失效。
  • 函数可以接收所有权后再“归还”给调用方。
  • 需要保留原值时用 clone,但要清楚它的深拷贝代价。
  • Copy 标量类型(如 i64bool)按位复制,不会被移动。
  • 通过引用借用可以彻底避免移动;用自定义 Drop 还能直观看到值的释放时机。

最小示例

let greeting = String::from("hello");
let length = consume(greeting); // greeting 被移动
// println!("{greeting}"); // ❌ 此处 greeting 已失效
println!("length = {length}");

运行代码

cd examples
cargo run -p rt_06_ownership
cargo test -p rt_06_ownership

完整源码已嵌入在本页中,建议对照下面的讲解逐段阅读。

代码讲解

Rust 没有 GC,靠所有权在编译期决定何时释放内存。三条规则:

  1. 每个值有且只有一个所有者。
  2. 同一时刻只有一个所有者。
  3. 所有者离开作用域,值被 drop

移动(move)。String 传进函数会移动所有权,调用后原变量失效, 函数体结束时值被释放:

fn consume(owned: String) -> usize {
    owned.len()
} // owned 在此被 drop

归还所有权。 在只有所有权的世界里,一种常见模式是“拿进来、改一改、还回去”, 用返回值重新绑定即可:

fn append_excl(mut owned: String) -> String {
    owned.push('!');
    owned
}
let text = append_excl(String::from("rust")); // text == "rust!"

clone 的代价。 如果你还想保留原值,可以显式 clone。它是深拷贝, 会在堆上复制一份独立的数据,因此修改副本不会影响原值:

let original = String::from("ocean");
let copy = original.clone(); // 复制了一份新数据

Copy 类型 vs 移动类型。i64bool 这类实现了 Copy 的标量类型不会被移动, 而是按位复制,传参后原变量仍可使用——这与 String 的移动行为形成对比。

用引用避免移动。&[String](借用)时调用方仍然完整持有数据, 这通常比 clone 更高效,也是下一章的主题:

fn total_length(parts: &[String]) -> usize {
    parts.iter().map(String::len).sum()
}

看见 drop。 示例里实现了一个带自定义 DropGuard,运行时它会打印创建与释放。 注意:同一作用域内,值的 drop 顺序与创建顺序相反,内层作用域的值先于外层释放。

常见错误

移动后继续使用原变量。 这是最经典的错误:

let s = String::from("hi");
let t = s;
println!("{s}"); // ❌ value borrowed here after move

修复方式是改用引用或 clone

let s = String::from("hi");
let t = s.clone();
println!("{s} {t}");

误以为所有类型都会移动。 i64boolchar 等是 Copy,赋值或传参后原变量照样能用, 不要为它们额外 clone

滥用 clone 掩盖问题。 编译器报“after move”时随手 .clone() 能编译通过, 但可能带来不必要的深拷贝开销。优先考虑能否改成借用 &T

练习

  • Guard 再创建两个实例放在不同作用域,预测并验证它们的 drop 顺序。
  • total_length 改成接收 Vec<String>(夺走所有权),观察调用方之后还能否使用,并对比 &[String] 的差异。
  • 写一个函数接收 String 并返回其大写形式,思考用“归还所有权”还是“返回新 String”更合适。

小结

所有权是 Rust 内存安全的核心。移动转移所有权,clone 进行深拷贝,Copy 类型自动按位复制, 借用 &T 则在不夺走所有权的前提下访问数据;所有者离开作用域时值被自动 drop

下一步

大多数时候我们不想转移所有权,而是临时借用。下一章学习引用与借用。

完整示例代码

下面是 examples/06_ownership/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/06_ownership/src/main.rs
//! 所有权与移动语义。
//!
//! Rust 没有垃圾回收(GC),靠所有权在编译期决定何时释放内存。三条核心规则:
//! 1. 每个值有且只有一个所有者;
//! 2. 同一时刻只有一个所有者;
//! 3. 所有者离开作用域时,值会被自动 `drop`(释放)。
//!
//! 本示例围绕这三条规则,演示移动、归还、clone 代价、Copy 类型、
//! 作用域触发的 drop,以及用引用避免移动等典型场景。

/// 把 `String` 的所有权移动进函数并消费它。
///
/// 调用后,调用方手中的原变量就失效了——它的所有权已经交给了 `owned`。
fn consume(owned: String) -> usize {
    owned.len()
    // owned 在这里离开作用域并被 drop,其堆内存被释放。
}

/// 接收所有权,做一些处理后再把所有权“归还”给调用方。
///
/// 这是只有所有权语义时常见的写法:拿进来、改一改、还回去。
fn append_excl(mut owned: String) -> String {
    owned.push('!');
    owned
}

/// 通过引用借用,不夺走所有权。
///
/// 调用方在调用后仍然完整持有自己的数据。
fn total_length(parts: &[String]) -> usize {
    parts.iter().map(String::len).sum()
}

/// 接收实现了 `Copy` 的标量类型。
///
/// `i64` 是 `Copy`,传参时按位复制,调用方的原变量依然可用——不会被移动。
fn double(n: i64) -> i64 {
    n * 2
}

/// 一个带自定义 `Drop` 的小结构体,用来直观看到“值在何时被释放”。
struct Guard {
    name: String,
}

impl Guard {
    fn new(name: &str) -> Self {
        println!("  [Guard] 创建 {name}");
        Self {
            name: name.to_string(),
        }
    }
}

impl Drop for Guard {
    /// 当 `Guard` 离开作用域时,Rust 自动调用这里。
    fn drop(&mut self) {
        println!("  [Guard] 释放 {}", self.name);
    }
}

fn main() {
    println!("== 1. 函数调用导致移动 ==");
    let greeting = String::from("hello");
    let length = consume(greeting);
    // 此处不能再用 greeting,它的所有权已经移动进了 consume。
    println!("被消费的长度 = {length}");

    println!("\n== 2. 把所有权归还给调用方 ==");
    let text = String::from("rust");
    let text = append_excl(text); // 移动进去,再用返回值重新绑定。
    println!("归还后的值 = {text}");

    println!("\n== 3. clone 的代价 ==");
    let original = String::from("ocean");
    let copy = original.clone(); // 深拷贝:在堆上复制了一份新数据。
    println!("original = {original}, copy = {copy}");

    println!("\n== 4. Copy 类型 vs 移动类型 ==");
    let n = 21_i64;
    let doubled = double(n); // n 是 Copy,按位复制,下一行仍可用 n。
    println!("n = {n}, doubled = {doubled}");

    println!("\n== 5. 传引用以避免移动 ==");
    let parts = vec![String::from("rust"), String::from("ocean")];
    println!("总长度 = {}", total_length(&parts));
    println!("parts 仍可用 = {parts:?}");

    println!("\n== 6. 作用域与 drop 的顺序 ==");
    let _outer = Guard::new("outer");
    {
        let _inner = Guard::new("inner");
        println!("  内层作用域结束前……");
    } // inner 在这里被 drop
    println!("  外层作用域继续运行……");
    // outer 在 main 结束时被 drop(注意:drop 顺序与创建顺序相反)。
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn consumes_and_measures() {
        assert_eq!(consume(String::from("hello")), 5);
    }

    #[test]
    fn returns_ownership_back() {
        let s = append_excl(String::from("hi"));
        assert_eq!(s, "hi!");
    }

    #[test]
    fn clone_is_independent() {
        let original = String::from("rust");
        let mut copy = original.clone();
        copy.push_str("acean");
        // 修改 clone 不影响原值,二者是独立的数据。
        assert_eq!(original, "rust");
        assert_eq!(copy, "rustacean");
    }

    #[test]
    fn copy_leaves_original_usable() {
        let n = 10_i64;
        let d = double(n);
        // n 是 Copy 类型,传参后依然可用。
        assert_eq!(n, 10);
        assert_eq!(d, 20);
    }

    #[test]
    fn borrows_without_taking() {
        let parts = vec![String::from("ab"), String::from("cde")];
        assert_eq!(total_length(&parts), 5);
        // parts 在借用后仍可访问。
        assert_eq!(parts.len(), 2);
    }
}
examples/06_ownership/Cargo.toml
[package]
name = "rt_06_ownership"
version.workspace = true
edition.workspace = true
publish.workspace = true