Rust 学习(6)-所有权规则、移动语义、Clone 与 Copy

文章目录

返回首页 | 上篇 | 下篇


理解所有权的核心规则 ------ 移动语义、Clone、Copy trait


一、为什么需要所有权?

在开始学习所有权之前,先来看看其他语言是怎么管理内存的:

语言 内存管理方式 优点 缺点
C/C++ 手动管理 性能最好 极易出错,
Java/Go/Python 垃圾回收(GC) 使用简单 运行时开销,STW问题
Rust 所有权 无GC,性能好 学习曲线陡

Rust 的所有权系统在编译时就能保证内存安全,既不需要 GC,也不会出现悬垂指针。这就是 Rust 最独特的特性。


二、所有权三大规则

所有权系统基于三条简单规则:

  1. 每个值有唯一的所有者(owner)
  2. 同一时刻只能有一个可变引用(下篇细讲)
  3. 当所有者离开作用域,值会被自动释放

作用域示例:

rust 复制代码
{                      // s 未声明
    let s = "hello";   // s 进入作用域,生效
    // 可以使用 s
}                      // s 离开作用域,drop 被调用,内存释放

对于 String 类型,离开作用域时会自动调用 drop 函数释放堆内存。


三、移动语义(Move)

3.1 问题引入

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}", s1);  // ❌ 编译错误!
    println!("{}", s2);  // ✅ 正常工作
}

为什么 s1 不能用了?这要从 String 的内存结构说起。

3.2 String 的内存结构

String 在内存中由三部分组成(存储在栈上):

log 复制代码
s1 (栈)
+---------+--------+----------+
|  ptr    |  len   | capacity |
+---------+--------+----------+
    |
    ↓
堆内存:[h][e][l][l][o]
  • ptr:指向堆内存的指针
  • len:当前长度(5)
  • capacity:总容量

3.3 浅拷贝的问题

如果执行 let s2 = s1,只拷贝栈上的三部分(浅拷贝),会出现问题:

log 复制代码
s1 (栈)                            s2 (栈)
+---------+--------+----------+    +---------+--------+----------+
|  ptr    |  len   | capacity |    |  ptr    |  len   | capacity |
+---------+--------+----------+    +---------+--------+----------+
    |                                    |
    └────────┬───────────────────────────┘
             ↓
      堆内存:[h][e][l][l][o]

当 s1 和 s2 都离开作用域时,同一块堆内存会被释放两次(double free),这是严重的内存错误。

3.4 Rust 的解决方案:移动语义

Rust 的做法是:let s2 = s1 时,将 s1 的栈数据移动到 s2,s1 被标记为无效。

rust 复制代码
let s1 = String::from("hello");
let s2 = s1;  // s1 的所有权移动到 s2

// s1 不再有效,编译器禁止使用
log 复制代码
移动后:
s1 (失效)                           s2 (有效)
+---------+--------+----------+    +---------+--------+----------+
|  ptr    |  len   | capacity |    |  ptr    |  len   | capacity |
+---------+--------+----------+    +---------+--------+----------+
                                        |
                                        ↓
                                   堆内存:[h][e][l][l][o]

这样 s2 离开作用域时只会释放一次内存。


四、简单类型的拷贝:Copy trait

4.1 整型的行为不同

rust 复制代码
fn main() {
    let x = 5;
    let y = x;  // x 没有被移动

    println!("x = {}, y = {}", x, y);  // ✅ 两个都可以用
}

为什么整型和 String 的行为不同?

因为整型是存储在栈上的,不需要堆分配。let y = x 只是把 5 这个值拷贝了一份。

4.2 Copy trait

实现 Copy trait 的类型,赋值时会隐式拷贝而不是移动:

rust 复制代码
// 实现了 Copy 的类型(部分)
// 整数:i8, i16, i32, i64, i128, isize, u8, ...
// 浮点数:f32, f64
// 布尔:bool
// 字符:char
// 元组:仅当所有元素都是 Copy 时,如 (i32, i32)

4.3 Copy 和 Drop 互斥

rust 复制代码
struct MyStruct {
    data: String,  // String 没有实现 Copy
}

// ❌ 编译错误:String 没有实现 Copy
// impl Copy for MyStruct {}

重要:任何需要 drop 处理(如释放堆内存)的类型都不能实现 Copy。


五、深拷贝:Clone trait

5.1 使用 clone()

如果你确实需要完整复制堆上的数据(深拷贝),使用 .clone():

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 完整的深拷贝

    println!("s1 = {}, s2 = {}", s1, s2);  // ✅ 两者都有效
}

内存结构(深拷贝后):

log 复制代码
s1 (栈)              s2 (栈)
+---------+--------+    +---------+--------+
|  ptr    |  len   |    |  ptr    |  len   |
+---------+--------+    +---------+--------+
    ↓                       ↓
堆:[h][e][l][l][o]     堆:[h][e][l][l][o]  (两份独立数据)

5.2 手动实现 Clone

可以为自定义类型实现 Clone:

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

fn main() {
    let p1 = Person {
        name: String::from("Alice"),
        age: 30,
    };
    let p2 = p1.clone();  // 深拷贝

    println!("p1: {:?}, p2: {:?}", p1, p2);  // 两者都可用
}

六、Copy vs Clone 对比

特性 Copy Clone
发生时机 隐式(赋值、传参时自动发生) 显式(需要调用 .clone())
性能开销 极低(栈拷贝,memcpy) 可能很大(堆数据需深拷贝)
实现条件 所有成员都必须是 Copy 类型 无限制,可为任意类型
能否自定义 不能自定义,只能是按位拷贝 可以自定义 clone() 方法
典型类型 整数、浮点、bool、char、数组 String、Vec、HashMap、自定义类型

选择建议

  • 小数据且频繁传递:考虑实现 Copy(如包装整数的结构体)
  • 需要完整拷贝:使用 Clone,但要考虑性能
  • 不确定:保持所有权转移(移动),这是最安全高效的

七、函数调用与所有权转移

7.1 传参时的移动

rust 复制代码
fn main() {
    let s = String::from("hello");
    take_ownership(s);   // s 移动到函数内
    // println!("{}", s);  // ❌ s 已无效

    let x = 5;
    make_copy(x);        // x 是 Copy 类型,不会移动
    println!("{}", x);   // ✅ 仍然有效
}

fn take_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string 离开作用域,内存释放

fn make_copy(some_int: i32) {
    println!("{}", some_int);
}  // 无内存释放

7.2 返回值转移所有权

rust 复制代码
fn main() {
    let s1 = gives_ownership();           // 返回值所有权给 s1
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);    // s2 移动进函数,返回值给 s3
}  // s1 和 s3 被 drop,s2 已移动,无事发生

fn gives_ownership() -> String {
    let s = String::from("hello");
    s  // 所有权移出
}

fn takes_and_gives_back(s: String) -> String {
    s  // 接收后返回
}

八、函数参数与返回值配对模式

如果需要在函数调用后继续使用原值,有几种模式:

模式 1:返回元组

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("'{}' 的长度是 {}", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)  // 返回原值和计算结果
}

但这种写法很繁琐,下一篇讲的引用才是更好的解决方案。

模式 2:使用 clone

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(s1.clone());  // 传入副本
    println!("'{}' 的长度是 {}", s1, len);   // s1 仍然有效
}

fn calculate_length(s: String) -> usize {
    s.len()
}

九、常见陷阱与最佳实践

陷阱 1:误以为赋值是深拷贝

rust 复制代码
let s1 = String::from("hello");
let s2 = s1;   // ❌ 误以为是拷贝,实际是移动
println!("{}", s1);  // 编译失败

陷阱 2:函数调用后还想用原值

rust 复制代码
fn process(s: String) {
    // 处理...
}

let s = String::from("hello");
process(s);
println!("{}", s);  // ❌ 所有权已转移

陷阱 3:对 Copy 类型误解

rust 复制代码
let v1: Vec<i32> = vec![1, 2, 3];
let v2 = v1;        // 移动(Vec 没有 Copy)
let v3 = v1.clone(); // 深拷贝

let arr1 = [1, 2, 3];  // 数组元素是 Copy
let arr2 = arr1;       // 拷贝(数组实现 Copy)

最佳实践总结

场景 推荐做法
需要在多处使用同一个 String 使用引用(下篇)
需要独立副本 使用 .clone()
小数据且频繁赋值 考虑使用 Copy 类型
不确定所有权 让 Rust 编译器告诉你

十、总结与速查

核心概念

概念 说明 示例
所有者 每个值有唯一所有者 let s = String::from()
移动 所有权转移,原值失效 let s2 = s1
Copy 隐式栈拷贝,需实现 Copy trait 整数、浮点数等
Clone 显式深拷贝,调用 .clone() s2 = s1.clone()
Drop 离开作用域自动释放内存 编译器自动调用

决策树
#mermaid-svg-J7ii9l8CinwMlwsQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J7ii9l8CinwMlwsQ .error-icon{fill:#552222;}#mermaid-svg-J7ii9l8CinwMlwsQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J7ii9l8CinwMlwsQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .marker.cross{stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J7ii9l8CinwMlwsQ p{margin:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label text{fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label span{color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label span p{background-color:transparent;}#mermaid-svg-J7ii9l8CinwMlwsQ .label text,#mermaid-svg-J7ii9l8CinwMlwsQ span{fill:#333;color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .node rect,#mermaid-svg-J7ii9l8CinwMlwsQ .node circle,#mermaid-svg-J7ii9l8CinwMlwsQ .node ellipse,#mermaid-svg-J7ii9l8CinwMlwsQ .node polygon,#mermaid-svg-J7ii9l8CinwMlwsQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .rough-node .label text,#mermaid-svg-J7ii9l8CinwMlwsQ .node .label text,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label,#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-J7ii9l8CinwMlwsQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .rough-node .label,#mermaid-svg-J7ii9l8CinwMlwsQ .node .label,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label,#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label{text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .node.clickable{cursor:pointer;}#mermaid-svg-J7ii9l8CinwMlwsQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .arrowheadPath{fill:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J7ii9l8CinwMlwsQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster text{fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster span{color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J7ii9l8CinwMlwsQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape p,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label rect,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J7ii9l8CinwMlwsQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J7ii9l8CinwMlwsQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 栈上
堆上




需要复制一个值
类型存储在...
实现 Copy?
需要独立副本?
隐式拷贝,无开销
移动语义
使用 .clone
使用引用,下篇见

重要规则

  1. 移动后不能使用原变量
  2. Copy 类型不会移动
  3. Clone 是显式深拷贝,有性能开销
  4. 所有需要释放内存的类型都不能实现 Copy

十一、思考题

  1. 以下代码能编译吗?为什么?

    rust 复制代码
    fn main() {
        let s = String::from("hello");
        let s2 = s;
        let s3 = s2.clone();
        println!("{} {}", s2, s3);
    }
  2. 设计一个结构体 Point,包含两个 i32 字段,能否实现 Copy?为什么?

  3. 以下代码的输出是什么?

    rust 复制代码
    fn main() {
        let mut x = 10;
        let y = x;
        x += 5;
        println!("x = {}, y = {}", x, y);
    }

参考链接

返回首页 | 上篇 | 下篇

相关推荐
kiss strong1 小时前
AI学习-langchain
学习·langchain
码云骑士1 小时前
65-AI-Agent基础原理-AutoGPT-四大架构全景解读
人工智能·python·架构
灵性(๑>ڡ<)☆2 小时前
Java学习笔记--面向对象高级(多态)
笔记·学习
Ulyanov2 小时前
刚体动力学方程——牛顿-欧拉与陀螺效应的奥秘
开发语言·python·目标跟踪·雷达电子对抗·导引头
观察员2 小时前
用 Streamlit + 智谱 AI 打造虚拟伴侣聊天机器人
python
AA陈超2 小时前
003 XiYou 西游 — P0 阶段实施计划
c++·笔记·学习·ue5
Cubar3 小时前
[一]开源ERP:odoo19二次开发问题-原库存模块问题
python·开源·odoo·开源erp
其实防守也摸鱼3 小时前
运维--安全与数据库
网络·数据库·学习·安全·安全威胁分析·数据库架构·软件安全
滴滴滴嘟嘟嘟.3 小时前
强化学习-PPO 奖励塑形实验:Pendulum-v1 中角速度惩罚权重的影响
开发语言·python