《Rust编程实战》系列第33篇
在上一篇文章中,我们学习了Rust所有权移动Move。当一个拥有堆资源的值被赋给另一个变量时,所有权通常会发生移动,原变量随即失效。
例如:
ini
fn main() {
let first = String::from("Rust");
let second = first;
println!("{}", second);
}
执行:
ini
let second = first;
后,字符串所有权从first移动到second,不能继续使用first。
如果业务中确实需要两份彼此独立的数据,就不能只使用Move,而需要:
rust
Clone
Clone允许显式复制一个值。对于String、Vec等拥有堆数据的类型,Clone通常会复制底层数据,因此也被称为深拷贝。
本文将介绍:
-
什么是Clone
-
Clone与Move的区别
-
Clone与Copy的区别
-
String、Vec和HashMap的Clone
-
结构体实现Clone
-
#[derive(Clone)] -
clone_from()的作用 -
引用调用clone的注意事项
-
Clone的性能成本
-
什么时候应该Clone
-
如何避免滥用Clone
什么是Clone
Clone是Rust标准库中的一个Trait,用于显式创建一个值的副本。
基本语法:
ini
let new_value = value.clone();
例如:
rust
fn main() {
let first = String::from("Rust");
let second = first.clone();
println!("first:{}", first);
println!("second:{}", second);
}
输出:
sql
first:Rust
second:Rust
调用clone()后:
sql
first仍然有效
second也有效
两者拥有独立的字符串数据。
Clone与Move的区别
Move不会完整复制堆数据,而是转移所有权:
ini
let first = String::from("Rust");
let second = first;
执行结果:
sql
first失效
second成为所有者
Clone会复制数据:
ini
let first = String::from("Rust");
let second = first.clone();
执行结果:
sql
first有效
second有效
可以简单理解:
Move是把数据交给另一个所有者。
Clone是重新创建一份独立数据。
String的Clone过程
一个String通常包含:
指针
长度
容量
实际字符串内容保存在堆上。
例如:
ini
let first = String::from("Rust");
可以简单理解为:
perl
first
├── pointer ──> Rust
├── length:4
└── capacity:4
执行:
ini
let second = first.clone();
后会创建新的堆内存:
sql
first ──> Rust
second ──> Rust
两份字符串内容相同,但对应不同的堆内存,因此可以独立修改。
rust
fn main() {
let first = String::from("Rust");
let mut second = first.clone();
second.push_str(" Language");
println!("{}", first);
println!("{}", second);
}
输出:
sql
Rust
Rust Language
修改second不会影响first。
Clone与Copy的区别
Clone和Copy都可以产生副本,但行为不同。
Clone是显式复制:
ini
let second = first.clone();
Copy是隐式复制:
ini
let first = 100;
let second = first;
由于i32实现了Copy,赋值后两个变量都能继续使用。
主要区别:
| 特性 | Clone | Copy |
|---|---|---|
| 是否显式调用 | 是 | 否 |
| 使用方式 | value.clone() |
let b=a |
| 是否可能有较高成本 | 是 | 通常很低 |
| 能否复制堆数据 | 可以 | 通常不可以 |
| 常见类型 | String、Vec | i32、bool、char |
| 实现Copy的类型也必须实现Clone,但实现Clone的类型不一定能实现Copy。 | ||
| 例如: |
arduino
String
实现了Clone,但没有实现Copy。
Vec的Clone
Vec<T>可以调用clone(),前提是内部元素实现Clone。
ini
fn main() {
let first = vec![10, 20, 30];
let mut second = first.clone();
second.push(40);
println!("{:?}", first);
println!("{:?}", second);
}
输出:
csharp
[10, 20, 30]
[10, 20, 30, 40]
如果是Vec<String>,Clone会复制每个字符串:
rust
fn main() {
let first = vec![
String::from("Tom"),
String::from("Alice"),
];
let second = first.clone();
println!("{:?}", first);
println!("{:?}", second);
}
这可能涉及多次堆内存分配,数据量较大时成本会比较明显。
HashMap的Clone
HashMap也可以Clone,但键和值都需要实现Clone。
rust
use std::collections::HashMap;
fn main() {
let mut first = HashMap::new();
first.insert(
String::from("Tom"),
100,
);
let second = first.clone();
println!("{:?}", first);
println!("{:?}", second);
}
对于大型HashMap,Clone可能复制大量键和值,应谨慎使用。
结构体实现Clone
自定义结构体默认不能调用clone()。
rust
struct User {
name: String,
age: u32,
}
下面的代码会报错:
ini
let second = first.clone();
因为User没有实现Clone。
最简单的方式是添加:
bash
#[derive(Clone)]
完整示例:
rust
#[derive(Clone, Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let first = User {
name: String::from("Tom"),
age: 20,
};
let second = first.clone();
println!("{:?}", first);
println!("{:?}", second);
}
编译器会根据字段自动生成Clone实现。
derive Clone的条件
结构体要自动实现Clone,所有字段都必须实现Clone。
例如:
rust
#[derive(Clone)]
struct Product {
name: String,
price: f64,
}
这里:
-
String实现Clone -
f64实现Clone因此
Product可以自动实现Clone。如果某个字段没有实现Clone,编译器会报错。
手动实现Clone
也可以自己实现Clone Trait:
rust
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
impl Clone for User {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
age: self.age,
}
}
}
调用:
ini
let second = first.clone();
手动实现适合:
-
需要自定义复制行为
-
某些字段不希望直接复制
-
Clone时需要重新生成字段
例如复制用户时重新生成编号:
rust
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
impl Clone for User {
fn clone(&self) -> Self {
Self {
id: self.id + 1000,
name: self.name.clone(),
}
}
}
不过大多数普通结构体使用#[derive(Clone)]即可。
枚举实现Clone
枚举也可以自动实现Clone:
scss
#[derive(Clone, Debug)]
enum Status {
Pending,
Success(String),
Failed(String),
}
只要所有变体中的数据都实现Clone,整个枚举就可以Clone。
rust
fn main() {
let first = Status::Success(
String::from("完成"),
);
let second = first.clone();
println!("{:?}", first);
println!("{:?}", second);
}
clone_from()
Clone Trait除了clone(),还提供:
scss
clone_from()
示例:
rust
fn main() {
let source = String::from("Hello Rust");
let mut target = String::from("Old");
target.clone_from(&source);
println!("{}", source);
println!("{}", target);
}
输出:
Hello Rust
Hello Rust
clone_from()会用source的副本替换target。
与下面写法类似:
ini
target = source.clone();
但某些类型可以复用target已经分配的内存,从而减少重新分配。
在普通业务代码中,clone()更常见;需要重复覆盖已有对象时,可以考虑clone_from()。
引用调用clone的注意事项
引用本身也实现Clone。
例如:
ini
fn main() {
let text = String::from("Rust");
let first = &text;
let second = first.clone();
println!("{}", first);
println!("{}", second);
}
这里的second可能仍然只是一个引用:
arduino
&String
而不是新的String。
如果需要复制真正的字符串,可以写:
ini
let second = (*first).clone();
或者:
ini
let second: String = first.clone();
具体结果会受到类型推断影响。
阅读代码时应区分:
复制引用
复制引用指向的数据
复制引用成本很低,但不会创建新的底层字符串。
Clone与函数参数
有些函数需要取得数据所有权,但调用者后面还要继续使用原变量,这时可以Clone。
rust
fn save_name(name: String) {
println!("保存:{}", name);
}
fn main() {
let name = String::from("Tom");
save_name(name.clone());
println!("原变量:{}", name);
}
这里函数得到一份独立数据,原变量仍然有效。
不过如果函数只需要读取字符串,更好的设计是:
rust
fn save_name(name: &str) {
println!("保存:{}", name);
}
调用:
scss
save_name(&name);
这样无需复制。
Clone的性能成本
Clone的成本取决于类型。
复制整数:
bash
10_i32.clone()
成本很低。
复制String:
scss
text.clone()
通常需要:
-
分配新的堆内存
-
复制全部字节
复制
Vec<String>还需要: -
分配新的Vec空间
-
遍历所有元素
-
克隆每个String
因此Clone的时间复杂度可能是:
scss
O(n)
而Move通常只需要转移少量元数据,成本接近:
scss
O(1)
所以面对大型数据时,应避免无意义Clone。
Clone并不一定是深拷贝
通常我们把String::clone()称为深拷贝,但Clone Trait本身并不强制要求"深拷贝"。
自定义类型可以决定Clone行为。
例如使用智能指针Rc<T>时:
ini
let second = first.clone();
通常只是增加引用计数,并不会复制底层数据。
因此更准确的说法是:
Clone创建一个逻辑上的副本,但具体如何复制由类型决定。
实战:复制商品模板
ini
#[derive(Clone, Debug)]
struct Product {
name: String,
price: f64,
stock: u32,
}
fn main() {
let template = Product {
name: String::from("Rust Book"),
price: 99.0,
stock: 0,
};
let mut first = template.clone();
first.stock = 100;
let mut second = template.clone();
second.name = String::from("Rust Advanced");
second.price = 129.0;
println!("模板:{:?}", template);
println!("商品一:{:?}", first);
println!("商品二:{:?}", second);
}
这种场景中,Clone用于基于模板创建多个独立对象,是合理的。
实战:保存历史状态
rust
#[derive(Clone, Debug)]
struct Config {
host: String,
port: u16,
}
fn main() {
let mut config = Config {
host: String::from("127.0.0.1"),
port: 8080,
};
let old_config = config.clone();
config.port = 9000;
println!("旧配置:{:?}", old_config);
println!("新配置:{:?}", config);
}
当业务需要保存修改前状态、实现撤销功能或生成快照时,Clone也很实用。
常见错误
为解决编译错误盲目Clone
看到:
borrow of moved value
就立即添加:
scss
clone()
虽然可能通过编译,但不一定是最佳方案。
应该先判断函数是否只需要借用。
大型循环中频繁Clone
不推荐:
ini
for item in &items {
results.push(item.clone());
}
如果只是读取,直接使用引用;如果需要转换,可以考虑迭代器和所有权转移。
误以为Clone没有成本
String、Vec、HashMap的Clone可能产生明显内存和CPU开销。
误把引用Clone当成数据Clone
ini
let new_ref = old_ref.clone();
可能只是复制引用,并没有复制底层数据。
为所有结构体都derive Clone
并不是所有业务类型都应该轻易复制。
例如:
-
数据库连接
-
文件句柄
-
锁
-
网络连接
这些资源通常不适合简单Clone。
Clone最佳实践
只读时优先借用
推荐:
php
fn print_user(user: &User)
而不是:
php
fn print_user(user: User)
再让调用者Clone。
确实需要两个所有者时再Clone
例如:
-
创建快照
-
保存历史版本
-
复制配置模板
-
发送给独立任务
大数据Clone要关注性能
对于大型Vec、String和HashMap,应评估复制次数和数据规模。
API设计要明确所有权
参数为:
arduino
String
表示函数可能接管数据。
参数为:
python
&str
表示函数只读取。
不要把Clone成本无条件转嫁给调用者。
优先让数据移动,而不是复制
如果原变量后面不再使用,直接Move通常更高效:
ini
let second = first;
不需要:
ini
let second = first.clone();
本章小结
Clone用于显式创建一个值的副本。
本文学习了:
-
clone()的基本使用 -
Clone与Move的区别
-
Clone与Copy的区别
-
String、Vec和HashMap的Clone
-
使用
#[derive(Clone)]实现Clone -
手动实现Clone Trait
-
枚举Clone
-
clone_from()的使用 -
引用Clone与数据Clone的区别
-
Clone可能产生堆内存分配和数据复制
-
Clone的具体行为由类型决定
-
只读场景应优先使用借用
-
只有真正需要独立副本时才使用Clone
可以记住:
Move转移所有权,通常成本较低。
Clone创建副本,可能涉及内存分配和完整数据复制。
Clone不是解决所有权错误的万能工具,能借用时应优先借用。
下一篇预告
下一篇我们将学习:
Rust Copy详解:隐式复制与轻量数据类型
内容包括:
-
什么是Copy Trait
-
Copy与Move的区别
-
Copy与Clone的关系
-
哪些类型实现Copy
-
元组和数组的Copy规则
-
结构体实现Copy
-
#[derive(Copy, Clone)] -
为什么String不能Copy
-
Copy类型在函数参数中的行为
-
什么时候不应该实现Copy