Rust Clone详解:深拷贝、复制成本与正确使用方式

《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允许显式复制一个值。对于StringVec等拥有堆数据的类型,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没有成本

StringVecHashMap的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

相关推荐
程序员-Benothing1 小时前
MySQL 的存储引擎有哪些?它们之间有什么区别?
后端·mysql·面试·职场和发展
卷无止境1 小时前
当Python遇上并发:concurrent.futures的核心逻辑与实战技巧
后端·python
卷无止境2 小时前
编程语言里到底有没有经济学规律?
后端·python
郑州光合科技余经理8 小时前
代驾系统架构拆解:订单链路、权限组织与私有化源码交付
开发语言·后端·算法·架构·系统架构·uni-app·php
hboot10 小时前
AI工程师第六课 - RAG检索增强生成
后端·langchain·llm
天才测试猿11 小时前
2026软件测试面试八股文(含答案+文档)
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
mldong11 小时前
jeeflow:98KB 的工作流引擎长什么样
后端
lanfordxb14 小时前
从 Markdown 到图片卡片:MarkCard Studio 的功能设计与官网实现记录
rust·软件工程·ai工具