所有权与移动
你会学到什么
- 每个值都有唯一的所有者,所有者离开作用域时值被自动释放。
- 把值传给函数或赋给新变量会发生“移动”,原变量随后失效。
- 函数可以接收所有权后再“归还”给调用方。
- 需要保留原值时用
clone,但要清楚它的深拷贝代价。 Copy标量类型(如i64、bool)按位复制,不会被移动。- 通过引用借用可以彻底避免移动;用自定义
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,靠所有权在编译期决定何时释放内存。三条规则:
- 每个值有且只有一个所有者。
- 同一时刻只有一个所有者。
- 所有者离开作用域,值被
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 移动类型。 像 i64、bool 这类实现了 Copy 的标量类型不会被移动,
而是按位复制,传参后原变量仍可使用——这与 String 的移动行为形成对比。
用引用避免移动。 传 &[String](借用)时调用方仍然完整持有数据,
这通常比 clone 更高效,也是下一章的主题:
fn total_length(parts: &[String]) -> usize {
parts.iter().map(String::len).sum()
}
看见 drop。 示例里实现了一个带自定义 Drop 的 Guard,运行时它会打印创建与释放。
注意:同一作用域内,值的 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}");
误以为所有类型都会移动。 i64、bool、char 等是 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