Rust Move 详解:所有权移动与变量失效

《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 不允许 firstsecond 同时拥有这块堆内存,因此会让 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);
}

nameString,因此发生 Move;pricef64,因此发生 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

相关推荐
AlienZHOU3 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
野生技术架构师4 小时前
2026 Java 面试全套总结,八股 + 场景 + AI 相关面试考点
java·人工智能·面试
狗头大军之江苏分军5 小时前
《潮水漫过十七岁》开学了
后端
苏三说技术6 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong6 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno7046 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc6 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
aramae7 小时前
模拟实现strlen()函数 (C语言)
c语言·开发语言·后端
kyriewen8 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒8 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端