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

相关推荐
snow@li1 小时前
SpringBoot:Payload统一响应包装/全景深入梳理
java·spring boot·后端
JaguarJack1 小时前
对标 npx 的 CPX PHP 的 Composer 包执行器
后端·php·服务端
BingoGo1 小时前
对标 npx 的 CPX PHP 的 Composer 包执行器
后端·php
艾莉丝努力练剑1 小时前
【MYSQL】MYSQL学习的一大重点:视图
android·服务器·数据库·学习·mysql·面试·视图
AINative软件工程2 小时前
LLM 多租户 Quota 工程实践:Token 配额、用量预警与自动熔断的生产设计
后端·llm·ai编程
艾莉丝努力练剑2 小时前
【MYSQL】MYSQL学习的一大重点:事务(下)- InnoDB 事务隔离性原理(MVCC 视角)
android·数据库·b树·sql·学习·mysql·面试
Ivanqhz2 小时前
php7 闭包实现
开发语言·后端·rust·php
有梦不弃10 小时前
模块化单体架构设计方案:DDD + 六边形架构落地实践
后端·架构
阿坤带你走近大数据10 小时前
基金面试准备1
面试·职场和发展