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

📅 2026/7/16 4:48:37 👁️ 阅读次数 📝 编程学习
Rust 学习(6)-所有权规则、移动语义、Clone 与 Copy

文章目录

    • 一、为什么需要所有权?
    • 二、所有权三大规则
    • 三、移动语义(Move)
      • 3.1 问题引入
      • 3.2 String 的内存结构
      • 3.3 浅拷贝的问题
      • 3.4 Rust 的解决方案:移动语义
    • 四、简单类型的拷贝:Copy trait
      • 4.1 整型的行为不同
      • 4.2 Copy trait
      • 4.3 Copy 和 Drop 互斥
    • 五、深拷贝:Clone trait
      • 5.1 使用 clone()
      • 5.2 手动实现 Clone
    • 六、Copy vs Clone 对比
    • 七、函数调用与所有权转移
      • 7.1 传参时的移动
      • 7.2 返回值转移所有权
    • 八、函数参数与返回值配对模式
    • 九、常见陷阱与最佳实践
    • 十、总结与速查
    • 十一、思考题
    • 参考链接

返回首页 | 上篇 | 下篇


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


一、为什么需要所有权?

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

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

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


二、所有权三大规则

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

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

作用域示例:

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

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


三、移动语义(Move)

3.1 问题引入

fnmain(){lets1=String::from("hello");lets2=s1;println!("{}",s1);// ❌ 编译错误!println!("{}",s2);// ✅ 正常工作}

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

3.2 String 的内存结构

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

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

3.3 浅拷贝的问题

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

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 被标记为无效。

lets1=String::from("hello");lets2=s1;// s1 的所有权移动到 s2// s1 不再有效,编译器禁止使用
移动后: s1 (失效) s2 (有效) +---------+--------+----------+ +---------+--------+----------+ | ptr | len | capacity | | ptr | len | capacity | +---------+--------+----------+ +---------+--------+----------+ | ↓ 堆内存:[h][e][l][l][o]

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


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

4.1 整型的行为不同

fnmain(){letx=5;lety=x;// x 没有被移动println!("x = {}, y = {}",x,y);// ✅ 两个都可以用}

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

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

4.2 Copy trait

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

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

4.3 Copy 和 Drop 互斥

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

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


五、深拷贝:Clone trait

5.1 使用 clone()

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

fnmain(){lets1=String::from("hello");lets2=s1.clone();// 完整的深拷贝println!("s1 = {}, s2 = {}",s1,s2);// ✅ 两者都有效}

内存结构(深拷贝后):

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

5.2 手动实现 Clone

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

#[derive(Debug, Clone)]structPerson{name:String,age:u32,}fnmain(){letp1=Person{name:String::from("Alice"),age:30,};letp2=p1.clone();// 深拷贝println!("p1: {:?}, p2: {:?}",p1,p2);// 两者都可用}

六、Copy vs Clone 对比

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

选择建议

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

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

7.1 传参时的移动

fnmain(){lets=String::from("hello");take_ownership(s);// s 移动到函数内// println!("{}", s); // ❌ s 已无效letx=5;make_copy(x);// x 是 Copy 类型,不会移动println!("{}",x);// ✅ 仍然有效}fntake_ownership(some_string:String){println!("{}",some_string);}// some_string 离开作用域,内存释放fnmake_copy(some_int:i32){println!("{}",some_int);}// 无内存释放

7.2 返回值转移所有权

fnmain(){lets1=gives_ownership();// 返回值所有权给 s1lets2=String::from("hello");lets3=takes_and_gives_back(s2);// s2 移动进函数,返回值给 s3}// s1 和 s3 被 drop,s2 已移动,无事发生fngives_ownership()->String{lets=String::from("hello");s// 所有权移出}fntakes_and_gives_back(s:String)->String{s// 接收后返回}

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

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

模式 1:返回元组

fnmain(){lets1=String::from("hello");let(s2,len)=calculate_length(s1);println!("'{}' 的长度是 {}",s2,len);}fncalculate_length(s:String)->(String,usize){letlength=s.len();(s,length)// 返回原值和计算结果}

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

模式 2:使用 clone

fnmain(){lets1=String::from("hello");letlen=calculate_length(s1.clone());// 传入副本println!("'{}' 的长度是 {}",s1,len);// s1 仍然有效}fncalculate_length(s:String)->usize{s.len()}

九、常见陷阱与最佳实践

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

lets1=String::from("hello");lets2=s1;// ❌ 误以为是拷贝,实际是移动println!("{}",s1);// 编译失败

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

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

陷阱 3:对 Copy 类型误解

letv1:Vec<i32>=vec![1,2,3];letv2=v1;// 移动(Vec 没有 Copy)letv3=v1.clone();// 深拷贝letarr1=[1,2,3];// 数组元素是 Copyletarr2=arr1;// 拷贝(数组实现 Copy)

最佳实践总结

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

十、总结与速查

核心概念

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

决策树

栈上

堆上

需要复制一个值

类型存储在...

实现 Copy?

需要独立副本?

隐式拷贝,无开销

移动语义

使用 .clone

使用引用,下篇见

重要规则

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

十一、思考题

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

    fnmain(){lets=String::from("hello");lets2=s;lets3=s2.clone();println!("{} {}",s2,s3);}
  2. 设计一个结构体 Point,包含两个 i32 字段,能否实现 Copy?为什么?

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

    fnmain(){letmutx=10;lety=x;x+=5;println!("x = {}, y = {}",x,y);}

参考链接

  • Rust Book - What is Ownership
  • Rust Book - Copy and Clone
  • Rust by Example - Ownership

返回首页 | 上篇 | 下篇