Rust语言Copy trait与Clone trait的区别

本文首发公众号 猩猩程序员 欢迎关注

在深入 CopyClone 之前,我们必须先理解 Rust 的核心特性:所有权

  1. 所有权规则

    • Rust 中的每个值都有一个被称为其 所有者(owner)的变量。
    • 一次只能有一个所有者。
    • 当所有者(变量)离开作用域时,这个值将被丢弃(drop)。
  2. 移动(Move)

    • 当我们将一个值赋给另一个变量,或者将它作为参数传递给函数时,默认情况下所有权会 移动(move)。
    • 一旦所有权被移动,原来的变量就不能再被访问,这可以防止"二次释放"(double free)等内存安全问题。
rust 复制代码
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到了 s2
// println!("{}", s1); // 这里会编译错误,因为 s1 不再有效

String 类型的数据存储在堆上,它包含一个指向堆内存的指针。移动操作只复制了这个指针、长度和容量(这些数据在栈上),而没有复制堆上的数据。这就是为什么原始变量失效的原因。


Clone Trait:显式的"深拷贝"

Clone Trait 提供了一种显式创建值副本的方法,通常是"深拷贝"。

  • 目的:当你需要一个值的独立副本,而不仅仅是转移所有权时使用。
  • 方法 :它提供了一个 clone() 方法。
  • 成本clone() 可能是昂贵的。例如,克隆一个 Vec<T>String 意味着需要在堆上分配新的内存,并复制所有元素。
  • 行为 :开发者可以为自己的类型自定义 clone() 的行为。

示例

rust 复制代码
let s1 = String::from("hello");
let s2 = s1.clone(); // 使用 clone() 创建一个深拷贝

println!("s1 = {}, s2 = {}", s1, s2); // s1 和 s2 都有效,因为 s2 是 s1 的一个完整副本

在这个例子中,s1.clone() 在堆上分配了新的内存,并将 "hello" 的内容复制到新内存中,s2 指向这个新内存。s1s2 是两个完全独立的 String 实例。


Copy Trait:隐式的"按位复制"

Copy Trait 允许一个类型在赋值操作时被复制而不是移动。这是一种特殊的 Clone,它的复制开销极小。

  • 目的:用于那些可以安全地进行"按位复制"(bitwise copy)的类型。
  • 行为 :如果一个类型实现了 Copy,那么在赋值、传参等操作时,它会自动创建一个副本,而不会使原始变量失效。这个过程是隐式的,不需要调用任何方法。
  • 成本:非常便宜,因为它只涉及在栈上的字节复制。
  • 约束
    1. 一个类型要实现 Copy,它必须首先实现 Clone。因为 Copy 的能力是 Clone 的一个子集(Copy is a marker trait that indicates that a type can be duplicated simply by copying bits)。
    2. 一个类型要能实现 Copy,它的所有成员(字段)也必须都实现了 Copy。这就是为什么像 StringVec<T> 这样在内部管理堆内存的类型不能实现 Copy 的原因,因为复制它们需要特殊的逻辑,而不仅仅是按位复制。

示例

所有基本类型,如 i32, f64, bool, char 以及只包含这些类型的元组(tuple)和结构体(struct)都默认实现了 Copy

rust 复制代码
let x = 5; // i32 实现了 Copy
let y = x; // x 的值被复制给了 y,x 仍然有效

println!("x = {}, y = {}", x, y); // 输出 x = 5, y = 5

自定义 Copy 类型

rust 复制代码
// #[derive(Copy, Clone)] 派生宏可以自动实现这两个 trait
#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1; // 因为 Point 实现了 Copy,p1 被复制到 p2,p1 仍然有效

println!("p1 = {:?}, p2 = {:?}", p1, p2); // p1 和 p2 都有效

如果我们尝试在一个包含非 Copy 类型(如 String)的结构体上派生 Copy,编译器会报错。

rust 复制代码
// #[derive(Copy, Clone)] // 这行会导致编译错误!
// struct Person {
//     name: String, // String 没有实现 Copy
//     age: u32,
// }

Copy vs Clone:核心区别总结

特性 Clone Copy
调用方式 显式调用 val.clone() 隐式发生(赋值、传参等)
所有权 clone() 创建新所有者,原所有者不受影响 赋值时创建新副本,原变量仍有效
性能开销 可能很高(如堆分配) 非常低(仅栈上按位复制)
适用类型 几乎任何类型都可以实现 仅限于其所有成员也都是 Copy 的类型
Trait 关系 CopyClone 的父 Trait (Copy: Clone) Clone 的一个特例
教案比喻 复印一本书:需要时间和资源,得到一本全新的书。 抄写一个电话号码:快速简单,原始号码还在那里。

何时使用?

  1. 默认使用 Clone :当你需要一个类型的副本时,首先考虑实现 Clone。这是最通用和最明确的方式。

  2. 谨慎使用 Copy :只为那些"简单"的、完全存储在栈上、并且复制开销很小的类型实现 Copy。通常是些小的结构体或枚举,它们表现得像 i32 这样的基本类型。

  3. API 设计

    • 如果你的函数需要获得数据的所有权,但调用者可能还想继续使用原始数据,那么可以接受一个实现了 Clone 的泛型参数 T,并在函数内部 clone() 它。
    • 如果你的函数接受的参数是小而简单的类型,可以要求它实现 Copy,这样函数调用会更简洁、高效。
rust 复制代码
// 接受一个可 Clone 的参数
fn process_data<T: Clone>(data: &T) {
    let my_copy = data.clone();
    // ...
}

// 接受一个可 Copy 的参数,直接按值传递
fn calculate<T: Copy>(value: T) {
    let another_value = value; // 隐式复制
    // ...
}

本文首发公众号 猩猩程序员 欢迎关注

相关推荐
孟祥_成都14 分钟前
前端角度学 AI - 15 分钟入门 Python
前端·人工智能
掘金安东尼15 分钟前
Astro 十一月更新:新特性与生态亮点(2025)
前端
拉不动的猪16 分钟前
判断dom元素是否在可视区域的常规方式
前端·javascript·面试
Hilaku35 分钟前
如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)
前端·javascript·面试
guxuehua38 分钟前
Monorepo Beta 版本发布问题排查与解决方案
前端
猫头虎-前端技术39 分钟前
小白也能做AI产品?我用 MateChat 给学生做了一个会“拍照解题 + 分步教学”的AI智能老师
前端·javascript·vue.js·前端框架·ecmascript·devui·matechat
b***666140 分钟前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
栀秋66641 分钟前
ES6+新增语法特性:重塑JavaScript的开发范式
前端·javascript
爱分享的鱼鱼43 分钟前
Vue动态路由详解:从基础到实践
前端
未来之窗软件服务1 小时前
幽冥大陆(三十七)文件系统路径格式化——东方仙盟筑基期
前端·javascript·文件系统·仙盟创梦ide·东方仙盟