《Rust 编程实战》系列第 32 篇
在上一篇文章中,我们学习了 Rust 所有权
Ownership,了解了每个值都有所有者、同一时间只能有一个所有者,以及所有者离开作用域后数据会自动释放。本篇继续深入所有权系统中最重要的行为之一:
scss
Move
中文通常称为所有权移动。
很多初学者第一次看到下面的代码时都会疑惑:
ini
fn main() {
let first = String::from("Rust");
let second = first;
println!("{}", first);
}
这段代码无法通过编译,因为执行:
ini
let second = first;
之后,字符串的所有权已经从 first 移动到 second,原变量 first 立即失效。
Move 并不是简单复制变量,而是 Rust 用来保证内存安全的重要机制。本文将介绍:
-
什么是 Move
-
Move 的执行过程
-
String 为什么会移动
-
Move 与浅拷贝的区别
-
函数参数中的 Move
-
函数返回值中的 Move
-
结构体和集合中的 Move
-
模式匹配中的 Move
-
部分移动
-
如何避免不必要的 Move
-
Move、Copy、Clone 与 Borrow 的选择
什么是 Move
Move 表示:
一个值的所有权从一个变量转移到另一个变量。
例如:
ini
fn main() {
let first = String::from("Rust");
let second = first;
println!("{}", second);
}
执行后:
sql
first 失效
second 成为新所有者
此时字符串数据并没有被完整复制,而是由 second 接管。
如果继续使用 first:
arduino
println!("{}", first);
编译器会提示类似错误:
go
borrow of moved value: `first`
Move 的执行过程
先看代码:
ini
let first = String::from("Rust");
let second = first;
一个 String 通常包含:
指针
长度
容量
变量 first 保存的并不是所有字符本身,而是用于管理堆内存的信息。
可以简单理解为:
perl
first
├── pointer ──> "Rust"
├── length: 4
└── capacity: 4
执行:
ini
let second = first;
Rust 会把 first 中的指针、长度和容量复制给 second。
此时:
perl
second
├── pointer ──> "Rust"
├── length: 4
└── capacity: 4
但 Rust 不允许 first 和 second 同时拥有这块堆内存,因此会让 first 立即失效。
最终只有:
sql
second
负责释放这块内存。
Move 为什么不是普通浅拷贝
从底层数据来看,Move 确实会复制指针、长度和容量,看起来很像浅拷贝。
区别在于:
普通浅拷贝通常让两个变量同时指向同一块内存,而 Rust Move 会让旧变量失效。
如果旧变量不失效,作用域结束时可能发生:
sql
first 释放内存
second 再次释放同一块内存
这会导致:
c
double free
重复释放可能造成程序崩溃和安全漏洞。
Rust 通过 Move 在编译阶段避免这个问题。
String 为什么会 Move
String 拥有堆内存资源,因此不能默认进行简单复制。
例如:
ini
fn main() {
let name = String::from("Tom");
let username = name;
println!("{}", username);
}
name 的所有权移动给 username。
而整数不会发生这种情况:
ini
fn main() {
let first = 100;
let second = first;
println!("{}", first);
println!("{}", second);
}
因为 i32 实现了 Copy,赋值时会直接复制值。
Move 与 Copy
Move:
ini
let first = String::from("Rust");
let second = first;
结果:
sql
first 失效
second 有效
Copy:
ini
let first = 100;
let second = first;
结果:
sql
first 有效
second 有效
常见 Copy 类型包括:
-
整数
-
浮点数
-
布尔值
-
字符
-
只包含 Copy 类型的元组
例如:
arduino
fn main() {
let point = (10, 20);
let other = point;
println!("{:?}", point);
println!("{:?}", other);
}
由于元组中的两个元素都是 i32,整个元组也可以 Copy。
函数参数中的 Move
将拥有所有权的值传入函数时,通常会发生 Move。
javascript
fn print_name(name: String) {
println!("{}", name);
}
fn main() {
let name = String::from("Alice");
print_name(name);
}
调用:
scss
print_name(name);
时,所有权从 main 中的 name 移动到函数参数 name。
函数结束后,参数离开作用域,字符串内存被释放。
下面的代码会报错:
rust
fn main() {
let name = String::from("Alice");
print_name(name);
println!("{}", name);
}
因为原变量已经失效。
Copy 类型传入函数
Copy 类型传入函数时,不会让原变量失效。
rust
fn print_age(age: u32) {
println!("{}", age);
}
fn main() {
let age = 20;
print_age(age);
println!("{}", age);
}
这里传入的是 age 的副本。
函数返回值中的 Move
函数返回拥有所有权的值时,所有权会移动给调用者。
rust
fn create_name() -> String {
let name = String::from("Rust");
name
}
fn main() {
let name = create_name();
println!("{}", name);
}
可以理解为:
css
函数内部 name
↓ Move
main 中的 name
函数结束时,字符串不会被释放,因为所有权已经成功返回。
传入后再返回所有权
可以通过返回值把所有权还给调用者:
rust
fn return_name(name: String) -> String {
name
}
fn main() {
let name = String::from("Tom");
let name = return_name(name);
println!("{}", name);
}
这种方式可以工作,但如果函数只需要读取数据,反复移动和返回所有权显得很繁琐。
更常见的做法是使用借用:
rust
fn print_name(name: &str) {
println!("{}", name);
}
结构体中的 Move
把 String 放入结构体时,所有权会移动到结构体字段。
rust
struct User {
name: String,
age: u32,
}
fn main() {
let name = String::from("Tom");
let user = User {
name,
age: 20,
};
println!("{}", user.name);
}
执行:
name,
后,原变量 name 失效。
现在字符串的所有者是:
user.name
从结构体中移动字段
也可以把结构体字段移动出来:
rust
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("Tom"),
age: 20,
};
let name = user.name;
println!("{}", name);
println!("{}", user.age);
}
user.name 被移动,但 user.age 是 Copy 类型,因此仍然可以访问。
不过不能再整体使用 user:
arduino
println!("{:?}", user);
因为结构体已经发生部分移动。
集合中的 Move
把 String 添加到 Vec 时,所有权会移动到集合中:
rust
fn main() {
let name = String::from("Tom");
let mut names = Vec::new();
names.push(name);
println!("{:?}", names);
}
调用:
ini
names.push(name);
后,原变量 name 失效。
现在 Vec 负责管理字符串。
从集合中取出拥有所有权的值时,也可能发生 Move:
rust
fn main() {
let mut names = vec![
String::from("Tom"),
String::from("Alice"),
];
let first = names.remove(0);
println!("{}", first);
println!("{:?}", names);
}
remove() 会把元素从集合中删除,并把所有权返回给调用者。
HashMap 中的 Move
将 String 作为键或值插入 HashMap 时,同样会移动所有权:
rust
use std::collections::HashMap;
fn main() {
let key = String::from("Tom");
let value = String::from("Admin");
let mut users = HashMap::new();
users.insert(key, value);
println!("{:?}", users);
}
执行后不能继续使用:
vbnet
key
value
因为它们的所有权已经移动到 HashMap。
模式匹配中的 Move
模式匹配可能会移动值。
rust
fn main() {
let value = Some(String::from("Rust"));
match value {
Some(text) => {
println!("{}", text);
}
None => {}
}
}
这里:
scss
Some(text)
会把内部 String 移动到变量 text。
匹配结束后,不能继续整体使用 value。
如果只想借用,可以写:
rust
fn main() {
let value = Some(String::from("Rust"));
match &value {
Some(text) => {
println!("{}", text);
}
None => {}
}
println!("{:?}", value);
}
if let 中的 Move
同样的问题也会出现在 if let:
arduino
fn main() {
let value = Some(String::from("Rust"));
if let Some(text) = value {
println!("{}", text);
}
}
这里内部字符串被移动。
如果希望保留原值,可以匹配引用:
arduino
if let Some(text) = &value {
println!("{}", text);
}
解构中的 Move
解构拥有所有权的数据时,字段可能被移动:
rust
struct Product {
name: String,
price: f64,
}
fn main() {
let product = Product {
name: String::from("Keyboard"),
price: 299.0,
};
let Product { name, price } = product;
println!("{} {}", name, price);
}
name 是 String,因此发生 Move;price 是 f64,因此发生 Copy。
闭包中的 Move
闭包也可能取得变量所有权:
ini
fn main() {
let message = String::from("Hello");
let print = move || {
println!("{}", message);
};
print();
}
关键字:
arduino
move
会强制闭包取得外部变量的所有权。
这在以下场景很常见:
-
创建线程
-
异步任务
-
长期保存闭包
例如线程:
rust
use std::thread;
fn main() {
let message = String::from("Hello");
let handle = thread::spawn(move || {
println!("{}", message);
});
handle.join().unwrap();
}
新线程可能比当前作用域存活更久,因此闭包需要拥有 message。
Move 与赋值
重新赋值会释放旧值:
vbnet
fn main() {
let mut text = String::from("Old");
text = String::from("New");
println!("{}", text);
}
执行新赋值前,旧字符串 "Old" 会被自动释放。
这不属于把旧值移动给新变量,而是原变量开始拥有新的值。
Move 与变量遮蔽
变量遮蔽也经常与 Move 一起出现:
arduino
fn normalize(text: String) -> String {
text.trim().to_lowercase()
}
fn main() {
let text = String::from(" RUST ");
let text = normalize(text);
println!("{}", text);
}
第一个 text 被移动进函数,函数返回新的 String,第二个同名变量接收返回值。
这种写法简洁且常见。
如何避免不必要的 Move
使用引用
如果函数只需要读取数据,不要取得所有权:
arduino
fn print_text(text: &str) {
println!("{}", text);
}
调用:
vbnet
let text = String::from("Rust");
print_text(&text);
println!("{}", text);
使用可变引用
如果函数需要修改数据,但不需要拥有它:
arduino
fn add_suffix(text: &mut String) {
text.push_str(" Language");
}
fn main() {
let mut text = String::from("Rust");
add_suffix(&mut text);
println!("{}", text);
}
确实需要独立数据时使用 clone
ini
let second = first.clone();
但要清楚这会复制堆数据。
调整函数设计
如果函数只是查询数据,可以返回引用,而不是返回新的拥有所有权的对象。
Move、Copy、Clone 与 Borrow 的选择
面对数据传递时,可以这样判断。
使用 Move:
-
函数需要长期保存数据
-
数据要进入结构体或集合
-
原变量之后不再使用
使用 Copy:
-
类型本身实现 Copy
-
数据较小且复制成本低
使用 Clone:
-
确实需要两份独立数据
-
两个所有者需要分别修改或长期保存
使用 Borrow:
-
只读取数据
-
临时修改数据
-
不需要转移所有权
例如只打印字符串:
php
fn print_name(name: &str)
比:
php
fn print_name(name: String)
更加通用。
实战:订单创建
rust
#[derive(Debug)]
struct Order {
customer: String,
product: String,
amount: f64,
}
fn create_order(
customer: String,
product: String,
amount: f64,
) -> Order {
Order {
customer,
product,
amount,
}
}
fn main() {
let customer = String::from("Tom");
let product = String::from("Rust Book");
let order = create_order(
customer,
product,
99.0,
);
println!("{:?}", order);
}
调用后:
customer
product
的所有权都移动到了 Order 中。
这是合理设计,因为订单需要长期保存这些数据。
实战:读取但不接管所有权
rust
#[derive(Debug)]
struct User {
name: String,
score: u32,
}
fn print_user(user: &User) {
println!("用户名:{}", user.name);
println!("积分:{}", user.score);
}
fn main() {
let user = User {
name: String::from("Alice"),
score: 100,
};
print_user(&user);
println!("{:?}", user);
}
函数只是读取用户,因此接收:
sql
&User
避免了整个结构体发生 Move。
常见错误
Move 后继续使用原变量
错误:
ini
let first = String::from("Rust");
let second = first;
println!("{}", first);
解决方案:
-
使用
second -
使用借用
-
确实需要两份数据时使用
clone()
函数只读却接收 String
不推荐:
arduino
fn show(text: String)
推荐:
php
fn show(text: &str)
从结构体中移动字段后整体使用结构体
错误:
ini
let name = user.name;
println!("{:?}", user);
可以改为借用字段:
ini
let name = &user.name;
匹配 Option 时意外移动
错误:
arduino
if let Some(text) = value {
}
println!("{:?}", value);
如果后面还要使用,改为:
arduino
if let Some(text) = &value {
}
用 clone 掩盖所有权设计问题
看到 Move 错误后立即写:
scss
clone()
虽然能编译,但可能产生不必要的内存分配。应该先判断是否只需要引用。
Move 最佳实践
所有权转移应有明确目的
例如把数据放入:
-
结构体
-
Vec
-
HashMap
-
线程
这类长期持有数据的对象时,Move 很合理。
只读访问优先借用
函数只是查看内容时,使用:
r
&T
&str
&[T]
修改但不保存时使用可变借用
r
&mut T
不要害怕 Move
Move 不是错误,而是一种安全的数据交接机制。编译错误通常是在提醒:
这个值已经交给其他所有者,原变量不能再负责它。
让 API 表达所有权意图
接收:
arduino
String
表示函数可能接管数据。
接收:
python
&str
表示函数只临时读取数据。
这种类型设计能让调用者清楚了解函数行为。
本章小结
Move 是 Rust 所有权系统中的核心行为。
本文学习了:
-
Move 表示所有权转移
-
Move 后旧变量会失效
-
String 移动的是指针、长度和容量管理权
-
Rust 通过让旧变量失效避免重复释放
-
Copy 类型赋值后原变量仍然有效
-
函数参数和返回值都会发生所有权转移
-
结构体、Vec 和 HashMap 可以接管数据所有权
-
模式匹配和解构可能移动内部值
-
move闭包会取得外部变量所有权 -
部分移动后不能整体使用原对象
-
使用借用可以避免不必要的 Move
-
确实需要独立副本时使用 Clone
可以记住:
Move 不是复制数据,而是交接数据的管理责任。
旧变量失效,是 Rust 防止重复释放和悬空指针的重要保障。
需要交出数据时使用 Move,只需要访问数据时使用 Borrow。
下一篇预告
下一篇我们将学习:
Rust Clone 详解:深拷贝、复制成本与正确使用方式
内容包括:
-
什么是 Clone
-
Clone 与 Move 的区别
-
Clone 与 Copy 的区别
-
String 和 Vec 的 Clone
-
结构体实现 Clone
-
#[derive(Clone)] -
clone_from() 的使用
-
引用调用 clone 的注意事项
-
Clone 的性能成本
-
什么时候应该 Clone
-
如何避免滥用 Clone