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 最独特的特性。
二、所有权三大规则
所有权系统基于三条简单规则:
- 每个值有唯一的所有者(owner)
- 同一时刻只能有一个可变引用(下篇细讲)
- 当所有者离开作用域,值会被自动释放
作用域示例:
{// 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 对比
| 特性 | Copy | Clone |
|---|---|---|
| 发生时机 | 隐式(赋值、传参时自动发生) | 显式(需要调用 .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 是显式深拷贝,有性能开销
- 所有需要释放内存的类型都不能实现 Copy
十一、思考题
以下代码能编译吗?为什么?
fnmain(){lets=String::from("hello");lets2=s;lets3=s2.clone();println!("{} {}",s2,s3);}设计一个结构体 Point,包含两个 i32 字段,能否实现 Copy?为什么?
以下代码的输出是什么?
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
返回首页 | 上篇 | 下篇
