Rust 所有权 Ownership 详解:理解内存安全的核心机制

《Rust 编程实战》系列第 31 篇

在上一篇文章中,我们学习了 Rust 集成测试。本篇将进入 Rust 最核心、也是最重要的知识之一:

复制代码
Ownership

中文通常称为所有权。

很多初学者第一次接触 Rust 时,会对下面的代码感到困惑:

ini 复制代码
fn main() {
    let name = String::from("Rust");
    let new_name = name;
    println!("{}", name);
}

这段代码无法通过编译,因为 name 的所有权已经转移给了 new_name

在其他语言中,变量赋值通常只是复制一个值,但 Rust 会根据数据类型决定:

  • 复制数据

  • 转移所有权

  • 借用数据

    所有权系统让 Rust 在没有垃圾回收器的情况下,仍然可以保证内存安全。

    本文将介绍:

  • 什么是所有权

  • 栈与堆

  • 所有权三条规则

  • 变量作用域

  • String 的内存结构

  • 所有权移动

  • Clone 与 Copy

  • 函数参数和返回值中的所有权

  • 所有权与内存安全

  • 常见错误和最佳实践

什么是所有权

所有权可以理解为:

每一块内存数据,都由某个变量负责管理。

当这个变量离开作用域时,Rust 会自动释放它拥有的数据。

例如:

rust 复制代码
fn main() {
    let message = String::from("Hello Rust");
    println!("{}", message);
}

变量 message 拥有字符串数据。当 main 函数结束时,message 离开作用域,Rust 自动释放字符串占用的内存。

开发者不需要手动调用:

scss 复制代码
free()

也不需要等待垃圾回收器扫描内存。

所有权三条规则

Rust 所有权系统可以总结为三条规则。

第一条:

Rust 中的每一个值都有一个所有者。

例如:

ini 复制代码
let name = String::from("Tom");

字符串 "Tom" 的所有者是变量 name

第二条:

一个值在同一时间只能有一个所有者。

例如:

ini 复制代码
let first = String::from("Rust");
let second = first;

执行后,字符串数据的所有者从 first 变成 second

第三条:

当所有者离开作用域时,值会被自动释放。

vbnet 复制代码
{
    let text = String::from("Hello");
    println!("{}", text);
}

代码块结束后,text 被销毁,它拥有的字符串内存也被释放。

变量作用域

作用域表示变量在程序中有效的范围。

rust 复制代码
fn main() {
    {
        let language = String::from("Rust");
        println!("{}", language);
    }
}

变量 language 只在内部代码块中有效。

下面的代码会报错:

rust 复制代码
fn main() {
    {
        let language = String::from("Rust");
    }
    println!("{}", language);
}

因为变量已经离开作用域。

Rust 在变量离开作用域时,会自动调用:

sql 复制代码
drop

释放对应资源。

栈与堆

理解所有权之前,需要简单了解栈和堆。

栈适合保存:

  • 固定大小的数据

  • 生命周期较短的数据

  • 可以快速复制的数据

    例如:

ini 复制代码
let age: i32 = 20;
let enabled: bool = true;

这些数据大小在编译阶段已经确定,通常直接存储在栈上。

堆适合保存:

  • 大小可能变化的数据

  • 运行时动态创建的数据

    例如:

ini 复制代码
let name = String::from("Rust");

String 的实际文本内容通常存储在堆上。

String 的内存结构

一个 String 变量本身通常包含三部分信息:

复制代码
指针
长度
容量

例如:

vbnet 复制代码
let text = String::from("Rust");

可以简单理解为:

arduino 复制代码
text
├── 指针:指向堆上的字符串数据
├── 长度:4
└── 容量:4或更大

真正的字符数据:

复制代码
Rust

存储在堆上。

变量本身保存的是管理这段堆内存的信息。

所有权移动

看下面的代码:

ini 复制代码
fn main() {
    let first = String::from("Rust");
    let second = first;
    println!("{}", second);
}

这段代码可以运行。

但下面的代码会报错:

ini 复制代码
fn main() {
    let first = String::from("Rust");
    let second = first;
    println!("{}", first);
}

原因是:

ini 复制代码
let second = first;

并没有完整复制堆上的字符串数据,而是把所有权从 first 转移给了 second

这个过程称为:

scss 复制代码
Move

移动后:

sql 复制代码
first 失效
second 成为新所有者

Rust 为什么要让原变量失效

假设 firstsecond 都可以继续使用,并且都认为自己拥有同一块堆内存。

当作用域结束时:

sql 复制代码
first 释放一次
second 再释放一次

就会产生重复释放:

c 复制代码
double free

这可能导致:

  • 程序崩溃

  • 内存破坏

  • 安全漏洞

    Rust 通过让旧变量立即失效,从编译阶段避免重复释放。

Clone 深拷贝

如果确实需要复制一份独立的堆数据,可以使用:

scss 复制代码
clone()

示例:

rust 复制代码
fn main() {
    let first = String::from("Rust");
    let second = first.clone();
    println!("{}", first);
    println!("{}", second);
}

此时内存中存在两份独立字符串:

rust 复制代码
first  -> Rust
second -> Rust

两者可以分别使用和释放。

不过 clone() 需要复制堆数据,可能存在性能开销。

因此不要为了逃避所有权问题而大量使用 clone()

Copy 类型

有些类型在赋值时不会移动,而是直接复制。

例如:

ini 复制代码
fn main() {
    let first = 100;
    let second = first;
    println!("{}", first);
    println!("{}", second);
}

这段代码可以正常运行。

因为整数实现了:

rust 复制代码
Copy

常见 Copy 类型包括:

  • 整数

  • 浮点数

  • 布尔值

  • 字符

  • 只包含 Copy 类型的元组

    例如:

ini 复制代码
let point = (10, 20);
let new_point = point;
println!("{:?}", point);

仍然可以使用 point

Move 与 Copy 的区别

Copy

ini 复制代码
let a = 10;
let b = a;

结果:

css 复制代码
a 有效
b 有效

Move

ini 复制代码
let a = String::from("Rust");
let b = a;

结果:

css 复制代码
a 失效
b 有效

可以简单记住:

栈上的简单类型通常可以 Copy。

拥有堆资源的类型通常会 Move。

函数参数中的所有权

把一个 String 传入函数时,所有权通常会移动。

javascript 复制代码
fn print_name(name: String) {
    println!("{}", name);
}

fn main() {
    let name = String::from("Tom");
    print_name(name);
}

调用后,name 不能继续使用:

arduino 复制代码
println!("{}", name);

会报错。

因为:

css 复制代码
main 中的 name
    ↓
移动到 print_name

函数结束后,参数 name 被销毁,字符串数据也被释放。

Copy 类型传入函数

如果传入的是 Copy 类型,则原变量仍然有效。

rust 复制代码
fn print_age(age: u32) {
    println!("{}", age);
}

fn main() {
    let age = 20;
    print_age(age);
    println!("{}", age);
}

因为 u32 实现了 Copy,传入函数时只是复制。

函数返回值转移所有权

函数可以把所有权返回给调用者。

rust 复制代码
fn create_name() -> String {
    String::from("Rust")
}

fn main() {
    let name = create_name();
    println!("{}", name);
}

这里字符串所有权从函数内部转移到变量 name

也可以接收一个值后再返回:

rust 复制代码
fn return_name(name: String) -> String {
    name
}

fn main() {
    let name = String::from("Tom");
    let name = return_name(name);
    println!("{}", name);
}

虽然可以这样处理,但频繁传入再返回会让代码很繁琐。

Rust 为此提供了借用机制,后续章节会详细讲解。

实战:商品名称处理

下面的函数取得商品名称所有权并进行处理:

rust 复制代码
fn normalize_product_name(name: String) -> String {
    name.trim().to_lowercase()
}

fn main() {
    let product = String::from("  Rust Book  ");
    let product = normalize_product_name(product);
    println!("{}", product);
}

输出:

复制代码
rust book

第一次声明:

rust 复制代码
let product = String::from(...)

调用函数后,原所有权被移动。

随后使用同名变量接收新字符串,这种写法称为变量遮蔽:

ini 复制代码
let product = normalize_product_name(product);

实战:创建用户

rust 复制代码
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn create_user(name: String, age: u32) -> User {
    User { name, age }
}

fn main() {
    let name = String::from("Alice");
    let user = create_user(name, 20);
    println!("{:?}", user);
}

调用后:

复制代码
name

的所有权已经移动到 User 结构体中。

不能继续访问原变量:

arduino 复制代码
println!("{}", name);

结构体字段的所有权

结构体可以拥有自己的数据:

rust 复制代码
struct Product {
    name: String,
    price: f64,
}

创建结构体:

ini 复制代码
let name = String::from("Keyboard");
let product = Product {
    name,
    price: 299.0,
};

此时 name 的所有权移动到:

复制代码
product.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 类型,因此仍然可以访问。

不过不能再整体使用:

arduino 复制代码
println!("{:?}", user);

因为结构体已经发生部分移动。

所有权与集合

String 放入 Vec 时,所有权会移动到集合中。

rust 复制代码
fn main() {
    let name = String::from("Tom");
    let mut names = Vec::new();
    names.push(name);
    println!("{:?}", names);
}

此时不能继续使用原来的 name

因为集合现在负责管理字符串数据。

drop 提前释放数据

通常变量在离开作用域时自动释放。

也可以使用:

scss 复制代码
drop()

提前释放:

arduino 复制代码
fn main() {
    let text = String::from("Hello");
    println!("{}", text);
    drop(text);
}

调用 drop(text) 后,text 不能继续使用。

drop() 常用于:

  • 提前释放锁

  • 提前关闭资源

  • 缩短借用范围

    普通字符串通常不需要手动调用。

所有权如何保证内存安全

所有权系统可以避免:

  • 使用已经释放的内存

  • 重复释放内存

  • 悬空指针

  • 多个位置同时修改同一数据

    这些问题在 C 和 C++ 中通常需要开发者手动管理。

    Rust 在编译阶段检查所有权规则,因此很多内存错误无法通过编译。

常见错误

Move 后继续使用变量

错误:

ini 复制代码
let first = String::from("Rust");
let second = first;
println!("{}", first);

解决方式包括:

  • 使用 second

  • 使用 clone()

  • 使用引用借用数据

把 String 传入函数后继续使用

错误:

ini 复制代码
let name = String::from("Tom");
print_name(name);
println!("{}", name);

如果函数只需要读取,应改为借用:

rust 复制代码
fn print_name(name: &str) {
    println!("{}", name);
}

滥用 clone

不推荐:

ini 复制代码
let second = first.clone();

仅仅为了消除编译错误。

应该先判断:

  • 是否真的需要两份数据

  • 是否可以借用

  • 是否可以重新设计所有权关系

混淆 Copy 和 Clone

Copy 是隐式复制:

ini 复制代码
let b = a;

Clone 是显式复制:

ini 复制代码
let b = a.clone();

拥有堆数据的类型通常不能 Copy,但可以 Clone。

所有权最佳实践

明确谁负责拥有数据

例如业务实体通常拥有自己的字段:

arduino 复制代码
struct User {
    name: String,
}

函数只读取数据时,不应该轻易取得所有权。

只读参数优先借用

不推荐:

php 复制代码
fn print_name(name: String)

更通用:

php 复制代码
fn print_name(name: &str)

确实需要独立数据时再 clone

例如数据需要分别保存到两个长期存在的对象中,可以使用 clone()

让所有权关系保持简单

避免一个值在多个函数之间反复移动和返回。合理使用结构体和借用,可以让代码更清晰。

本章小结

所有权是 Rust 内存安全的核心。

本文学习了:

  • 每个值都有一个所有者

  • 同一时间只能有一个所有者

  • 所有者离开作用域时数据自动释放

  • 栈和堆的基本区别

  • String 的指针、长度和容量

  • 所有权移动 Move

  • clone() 深拷贝

  • Copy 类型

  • 函数参数和返回值中的所有权转移

  • 结构体与集合中的所有权

  • 部分移动

  • drop() 提前释放资源

  • 所有权如何避免重复释放和悬空指针

    可以记住:

Rust 不是禁止变量赋值,而是要求明确数据由谁负责管理。

简单类型通常复制,拥有资源的类型通常移动。

所有权让 Rust 在没有垃圾回收器的情况下,仍然能够自动并安全地管理内存。

下一篇预告

下一篇我们将继续学习:

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

内容包括:

  • Move 的完整执行过程

  • String 为什么会移动

  • Move 与浅拷贝的区别

  • 函数调用中的 Move

  • 结构体字段移动

  • 模式匹配中的 Move

  • 集合中的 Move

  • 如何避免不必要的 Move

  • Clone、Copy 与 Borrow 的选择

  • 常见所有权错误分析

相关推荐
JaguarJack1 小时前
PHP 引用计数机制深度解析
后端·php·服务端
Mh9 小时前
别再只会 `find` 了:Map 在前端业务里的真实用法
前端·javascript
陈随易10 小时前
FFmpeg 9.0 发布,代号 Lei,音视频处理再升级
前端·后端·程序员
爱丶狸10 小时前
Grafana_Zabbix_ImageRenderer_部署与前端操作手册
linux·前端·zabbix·grafana·kylin
k4m7v2pz12 小时前
把 Bevy 0.14 游戏移植到 R36S 掌机:一场与“无窗口系统“的搏斗
linux·rust·bevy·r36s·rk3326·开源掌机
doiito12 小时前
【Agent Harness】流马(Gliding Horse)DeepSeek Responses API 接入介绍
rust·系统设计·ai agent
凌虚13 小时前
面向 MySQL 用户的 PostgreSQL 快速上手指南
数据库·后端·架构
用户0595401744613 小时前
把AI对话记忆存储测试从手工改成Playwright+pytest,覆盖率从20%提到96%,回归时间缩短90%
前端·css
kyriewen13 小时前
别再这样写TypeScript了——Code Review中最常见的8个反模式
前端·javascript·typescript