《Rust编程实战》系列第34篇
在上一篇文章中,我们学习了Rust的
Clone,了解了如何通过:
php
value.clone()
显式创建一个值的副本。
对于String、Vec、HashMap等拥有堆数据的类型,clone()通常会复制底层数据,因此可能产生额外的内存分配和性能开销。
本篇继续学习Rust所有权体系中的另一个重要Trait:
rust
Copy
Copy与Clone都和"复制"有关,但它们的使用方式和适用类型完全不同。
例如:
ini
fn main() {
let first = 100;
let second = first;
println!("{}", first);
println!("{}", second);
}
这里虽然执行了:
ini
let second = first;
但first并没有失效。
原因是:
rust
i32
实现了Copy Trait,因此赋值时发生的是隐式复制,而不是所有权移动。
本文将介绍:
-
什么是Copy
-
Copy与Move的区别
-
Copy与Clone的关系
-
哪些类型实现Copy
-
元组和数组的Copy规则
-
结构体实现Copy
-
#[derive(Copy, Clone)] -
为什么String不能Copy
-
Copy在函数参数中的行为
-
Copy在模式匹配中的行为
-
什么时候应该实现Copy
-
什么时候不应该实现Copy
什么是Copy
Copy是Rust标准库中的一个标记Trait。
当一个类型实现Copy后,变量赋值时不会发生所有权移动,而是自动复制值。
例如:
ini
fn main() {
let first: i32 = 100;
let second = first;
println!("first = {}", first);
println!("second = {}", second);
}
输出:
ini
first = 100
second = 100
这里:
ini
let second = first;
相当于复制了first中的数据。
执行后:
sql
first 有效
second 有效
而如果是String:
ini
fn main() {
let first = String::from("Rust");
let second = first;
println!("{}", second);
}
执行后:
sql
first 失效
second 有效
因为String没有实现Copy。
Copy与Move的区别
Move表示转移所有权:
ini
let first = String::from("Rust");
let second = first;
结果:
sql
first失效
second成为所有者
Copy表示复制值:
ini
let first = 100;
let second = first;
结果:
sql
first有效
second有效
可以简单记住:
Move是把原来的值交出去。
Copy是保留原值,再复制一份新值。
为什么需要Copy
对于整数、布尔值这样的简单数据,如果每次赋值都发生Move,会让代码非常难写。
例如:
ini
let age = 20;
let new_age = age;
如果age立即失效,那么简单数字在函数之间传递也需要频繁借用。
实际上,复制一个:
rust
i32
只需要复制几个字节,成本很低。
因此Rust允许这类轻量类型实现Copy,让使用体验更加自然。
常见Copy类型
Rust中很多基础类型都实现了Copy。
整数类型
例如:
rust
i8
i16
i32
i64
i128
isize
u8
u16
u32
u64
u128
usize
示例:
ini
let a = 10u32;
let b = a;
println!("{} {}", a, b);
浮点类型
rust
f32
f64
示例:
ini
let price = 99.9;
let new_price = price;
println!("{} {}", price, new_price);
布尔类型
arduino
bool
ini
let enabled = true;
let status = enabled;
println!("{} {}", enabled, status);
字符类型
arduino
char
ini
let letter = 'R';
let copy = letter;
println!("{} {}", letter, copy);
不可变引用
普通共享引用:
r
&T
通常实现Copy。
例如:
ini
fn main() {
let text = String::from("Rust");
let first = &text;
let second = first;
println!("{}", first);
println!("{}", second);
}
这里复制的是引用本身,而不是复制String数据。
Copy与Clone的关系
Copy和Clone并不是互相独立的。
Rust规定:
一个类型如果实现Copy,也必须实现Clone。
因此:
rust
Copy: Clone
可以理解为Copy建立在Clone之上。
对于Copy类型:
ini
let a = 10;
let b = a;
是隐式复制。
也可以显式调用:
ini
let b = a.clone();
例如:
ini
fn main() {
let a = 100;
let b = a.clone();
println!("{} {}", a, b);
}
虽然可以这样写,但对于i32这类Copy类型,一般没有必要显式调用clone()。
直接:
ini
let b = a;
更自然。
Copy不是深拷贝
Copy通常只是按位复制一个值。
例如:
ini
let a: i32 = 100;
let b = a;
复制的是这个整数本身。
对于引用:
ini
let first = &text;
let second = first;
复制的只是引用地址。
此时:
sql
first ─┐
├──> 同一个text
second ┘
底层String并没有复制。
所以:
Copy只表示复制当前值成本低且语义安全,并不表示复制它引用的所有底层数据。
元组什么时候实现Copy
元组是否实现Copy,取决于所有元素是否都实现Copy。
例如:
ini
fn main() {
let first = (10, 20.5, true);
let second = first;
println!("{:?}", first);
println!("{:?}", second);
}
这里:
-
i32实现Copy -
f64实现Copy -
bool实现Copy所以整个元组也可以Copy。
但如果元组包含
String:
rust
fn main() {
let first = (
1,
String::from("Tom"),
);
let second = first;
println!("{:?}", second);
}
这时整个元组不能Copy,因为其中的String不能Copy。
因此first发生Move。
数组什么时候实现Copy
数组也遵循类似规则。
例如:
ini
fn main() {
let first = [1, 2, 3];
let second = first;
println!("{:?}", first);
println!("{:?}", second);
}
数组元素都是i32,因此数组可以Copy。
如果数组包含String:
rust
fn main() {
let first = [
String::from("A"),
String::from("B"),
];
let second = first;
println!("{:?}", second);
}
此时发生Move,不能继续整体使用first。
结构体默认不会实现Copy
自定义结构体默认没有Copy。
例如:
rust
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let first = Point {
x: 10,
y: 20,
};
let second = first;
println!("{:?}", second);
}
此时first被移动。
即使字段全部是i32,Rust也不会自动让自定义结构体实现Copy。
如果需要,可以显式添加:
bash
#[derive(Copy, Clone)]
结构体实现Copy
示例:
rust
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let first = Point {
x: 10,
y: 20,
};
let second = first;
println!("{:?}", first);
println!("{:?}", second);
}
现在两个变量都可以使用。
注意:
bash
#[derive(Copy, Clone)]
通常两个一起写。
因为实现Copy的类型必须同时实现Clone。
结构体实现Copy的条件
结构体中的所有字段都必须实现Copy。
下面可以:
rust
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
因为两个字段都是Copy类型。
下面不可以:
rust
#[derive(Copy, Clone)]
struct User {
name: String,
age: u32,
}
因为:
arduino
String
没有实现Copy。
编译器会直接拒绝。
为什么String不能Copy
这是Copy最重要的问题之一。
String内部通常保存:
指针
长度
容量
真正文本位于堆内存。
假设String可以Copy:
ini
let first = String::from("Rust");
let second = first;
如果只是复制指针、长度和容量,那么:
sql
first ─┐
├──> 同一块Rust堆内存
second ─┘
当作用域结束时:
sql
first释放一次
second再次释放
会造成double free。
如果Copy时完整复制堆数据,又会产生隐藏的昂贵内存操作。
Rust的设计原则是:
Copy应该是低成本、可预测的隐式复制。
因此
String不能实现Copy,而需要显式:
scss
clone()
这样代码能够明确告诉读者:
这里可能发生真正的数据复制。
Vec为什么不能Copy
Vec<T>与String类似,也管理堆内存:
指针
长度
容量
因此:
ini
let first = vec![1, 2, 3];
let second = first;
发生Move。
如果需要两个独立Vec:
ini
let second = first.clone();
Copy在函数参数中的行为
Copy类型传给函数时,原变量仍然有效。
例如:
rust
fn print_age(age: u32) {
println!("年龄:{}", age);
}
fn main() {
let age = 20;
print_age(age);
println!("main中的age:{}", age);
}
调用:
scss
print_age(age);
时传入的是age的副本。
函数内部参数销毁不会影响原变量。
非Copy类型作为函数参数
如果参数是String:
javascript
fn print_name(name: String) {
println!("{}", name);
}
fn main() {
let name = String::from("Tom");
print_name(name);
}
所有权会Move到函数中。
之后不能继续使用:
arduino
println!("{}", name);
如果函数只需要读取,可以改为:
rust
fn print_name(name: &str) {
println!("{}", name);
}
这就进入下一篇要讲的Borrow借用。
Copy在函数返回值中的行为
返回Copy类型时,本质上得到一个独立副本:
rust
fn get_score() -> i32 {
let score = 100;
score
}
fn main() {
let score = get_score();
println!("{}", score);
}
对于Copy类型,不需要考虑堆资源所有权转移问题。
Copy与模式匹配
Copy类型在模式匹配中通常不会导致原值失效。
rust
fn main() {
let value = Some(100);
match value {
Some(number) => {
println!("{}", number);
}
None => {}
}
println!("{:?}", value);
}
Option<i32>本身可以Copy,因此匹配后value仍然可以使用。
如果是:
rust
Option<String>
情况就不同:
rust
fn main() {
let value = Some(String::from("Rust"));
match value {
Some(text) => {
println!("{}", text);
}
None => {}
}
}
内部String会被移动。
如果后面还要使用value,可以使用引用匹配:
arduino
match &value {
Some(text) => println!("{}", text),
None => {}
}
Copy与闭包
Copy类型被闭包捕获时,也通常更简单。
例如:
ini
fn main() {
let number = 100;
let show = move || {
println!("{}", number);
};
show();
println!("{}", number);
}
即使使用了:
arduino
move
由于number实现Copy,闭包得到的是副本,外部的number仍然有效。
如果是String,move闭包通常会接管所有权。
手动实现Copy
Copy Trait本身没有需要实现的方法:
kotlin
trait Copy: Clone {}
因此可以手动实现:
rust
#[derive(Clone)]
struct Point {
x: i32,
y: i32,
}
impl Copy for Point {}
不过实际开发中更推荐:
bash
#[derive(Copy, Clone)]
更加简洁,也不容易出错。
什么时候适合实现Copy
适合Copy的类型通常具有以下特点:
-
数据量很小
-
没有管理堆资源
-
复制成本很低
-
复制后两个值完全独立
例如:
rust
#[derive(Debug, Copy, Clone)]
struct Position {
x: i32,
y: i32,
}
坐标非常适合Copy。
又例如:
rust
#[derive(Debug, Copy, Clone)]
struct Size {
width: u32,
height: u32,
}
这种轻量值对象也适合Copy。
枚举实现Copy
简单枚举也非常适合Copy:
csharp
#[derive(Debug, Copy, Clone)]
enum Status {
Pending,
Running,
Finished,
}
示例:
ini
fn main() {
let first = Status::Running;
let second = first;
println!("{:?}", first);
println!("{:?}", second);
}
如果枚举变体包含的所有数据都是Copy,也可以实现Copy:
scss
#[derive(Debug, Copy, Clone)]
enum Value {
Integer(i32),
Boolean(bool),
}
什么时候不应该实现Copy
管理堆资源
例如:
rust
String
Vec<T>
HashMap<K,V>
不应该Copy。
对象复制成本较高
如果结构体非常大,即使字段都可以Copy,也不一定需要实现Copy。
类型有明确唯一身份
例如:
数据库连接
文件句柄
锁
网络连接
事务
这种资源通常不应该产生隐式副本。
希望显式体现复制操作
某些业务对象即使可以复制,也希望开发者明确调用:
scss
clone()
这样代码语义更清楚。
Copy和Clone应该怎么选
如果类型非常轻量:
Point
Size
Status
可以考虑:
rust
Copy + Clone
如果类型拥有堆资源:
rust
String
Vec
通常:
rust
Clone
但不能Copy。
如果类型根本不应该被复制:
文件
连接
锁
唯一资源
可以:
rust
既不Copy,也不Clone
实战:商品尺寸
arduino
#[derive(Debug, Copy, Clone)]
struct Size {
width: f64,
height: f64,
}
fn calculate_area(size: Size) -> f64 {
size.width * size.height
}
fn main() {
let size = Size {
width: 10.0,
height: 20.0,
};
let area = calculate_area(size);
println!("面积:{}", area);
println!("尺寸:{:?}", size);
}
因为Size实现了Copy,传入函数后仍然可以继续使用原变量。
这类小型值对象使用Copy非常方便。
实战:订单状态
rust
#[derive(Debug, Copy, Clone, PartialEq)]
enum OrderStatus {
Pending,
Paid,
Shipped,
Finished,
}
fn is_finished(status: OrderStatus) -> bool {
status == OrderStatus::Finished
}
fn main() {
let status = OrderStatus::Paid;
let result = is_finished(status);
println!("是否完成:{}", result);
println!("当前状态:{:?}", status);
}
状态枚举通常数据很小,非常适合Copy。
常见错误
误以为所有赋值都会Copy
错误理解:
ini
let second = first;
总是复制。
实际上:
-
Copy类型:复制
-
非Copy类型:Move
尝试让String结构体实现Copy
错误:
rust
#[derive(Copy, Clone)]
struct User {
name: String,
}
无法通过编译。
为大型对象随意实现Copy
Copy是隐式发生的,如果对象很大,会让复制成本变得不明显。
把Copy和clone混为一谈
Copy:
ini
let b = a;
隐式。
Clone:
ini
let b = a.clone();
显式。
认为引用Copy会复制底层数据
ini
let b = a_ref;
通常只是复制引用,并不会复制原对象。
Copy最佳实践
小型值对象优先考虑Copy
例如坐标、尺寸、状态、ID包装类型。
有堆资源时使用Clone或Move
例如:
rust
String
Vec<T>
不要试图实现Copy。
不要为了方便而实现Copy
应该根据类型语义决定,而不是为了减少所有权编译错误。
复杂对象优先借用
如果函数只是读取对象:
php
fn print_user(user: &User)
通常比复制整个对象更合理。
明确区分值语义和资源语义
例如:
css
Position
更像"值",适合Copy。
DatabaseConnection
更像"资源",不适合Copy。
本章小结
Copy是Rust所有权系统中用于轻量数据隐式复制的重要Trait。
本文学习了:
-
Copy类型赋值不会发生Move
-
Copy是一种隐式复制
-
实现Copy的类型必须同时实现Clone
-
整数、浮点数、bool、char通常实现Copy
-
共享引用通常可以Copy
-
元组和数组只有在内部元素全部Copy时才能Copy
-
自定义结构体可以使用
#[derive(Copy, Clone)] -
String、Vec等拥有堆资源的类型不能Copy
-
Copy类型传入函数后原变量仍然有效
-
Copy类型在模式匹配和move闭包中的行为
-
小型值对象和状态枚举适合Copy
-
资源类型和大型对象通常不适合Copy
可以记住:
Move转移所有权。
Clone显式创建副本。
Copy隐式创建轻量副本。
Copy不是为了绕过所有权,而是用于复制成本低、复制语义明确的数据类型。
下一篇预告
下一篇我们将正式学习Rust所有权系统中另一个核心概念:
Rust Borrow借用详解:不转移所有权访问数据
内容包括:
-
什么是Borrow
-
为什么需要借用
-
引用
&T -
可变引用
&mut T -
借用与Move的区别
-
借用规则
-
多个不可变引用
-
唯一可变引用
-
可变与不可变借用冲突
-
函数参数中的Borrow
-
Dereference解引用
-
借用与String、Vec、Struct
-
借用检查器Borrow Checker
-
常见编译错误与实战案例