Skip to content

Rust 中的拷贝和克隆 ​

克隆(Clone) ​

克隆是一种深拷贝(deep copy)机制,可以用于任何类型,包括堆上分配的数据:

  1. 定义特点:通过实现 Clone 特征并调用 clone() 方法,可能会复制堆上的数据。
  2. 适用类型:
    • 所有类型都可以实现 Clone 特征
    • 例如,String、Vec<T>、Box<T> 等
  3. 行为特点:
    • Rust 永远也不会自动创建数据的 深拷贝。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
    • 如果需要深度复制数据,可以实现 Clone 特征,同时必须实现 clone 方法
    • 会复制所有数据,包括堆上分配的数据,性能开销可能较大,特别是对于大型数据结构
rust
let s1 = String::from("hello");  
let s2 = s1.clone();  // 显式克隆,s1和s2都有效  
println!("s1 = {}, s2 = {}", s1, s2);  // 正常工作

这段代码能够正常运行,说明 s2 确实完整的复制了 s1 的数据。

如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 clone 来简化编程。但是对于执行较为频繁的代码(热点路径),使用 clone 会极大的降低程序性能,需要小心使用!

👀补充

虽然克隆会调用 clone,但并非所有的 clone 都是深拷贝。例如 Rc、迭代器等。它们的 clone 仅仅只是拷贝了引用指针,属于浅拷贝。

拷贝(Copy) ​

拷贝是一种浅拷贝(shallow copy)机制,只适用于存储在栈上的简单数据类型:

  1. 特点:通过实现 Copy 特征来启用,复制只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。
  2. 适用类型:
    • 所有整数类型,如 u32、i32
    • 布尔类型 bool
    • 浮点数类型,如 f64
    • 字符类型 char
    • 元组,当且仅当其包含的类型也都实现了 Copy 特征,比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
    • 不可变引用&T(但可变引用 &mut T 不可 Copy)
  3. 行为特点:
    • 自动发生,不需要显式调用任何方法(例如 clone)
    • 实现 Copy 必须先实现 Clone
    • 复制时不会发生所有权转移,复制后,原值和新值都可以使用,修改一个不会影响另一个
rust
fn main() {
    let x = 5;
    let y = x; // x被复制给y,x仍然可用,这里没有发生所有权的转移
    println!("x: {}, y: {}", x, y); // 输出:x: 5, y: 5
}

这段代码没有调用 clone,不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。原因是像整型这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效(x、y 都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。

复制值的特征 Copy 和 Clone ​

在 Rust 中,Copy 和 Clone 是两个用于对象复制的 trait。它们之间有一些重要的区别。 Clone 特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。

派生 Clone 实现了 clone 方法,当为整个的类型实现 Clone 时,在该类型的每一部分上都会调用 clone 方法。这意味着类型中所有字段或值也必须实现了 Clone,这样才能够派生 Clone 。

例如,当在一个切片(slice)上调用 to_vec 方法时, Clone 是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 to_vec 中返回的 Vector 需要拥有实例数据,因此, to_vec 需要在每个元素上调用 clone 来逐个复制。因此,存储在切片中的类型必须实现 Clone。

一个类型要实现 Copy 必须先实现 Clone ,因为 Copy 是 Clone 的子特征。当性能是关键因素时,应该优先考虑 Copy 而不是 Clone,因为深拷贝会导致性能下降。 Copy 特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),

当一个类型的内部字段全部实现了 Copy 时,你就可以在该类型上派上 Copy 特征。 一个类型如果要实现 Copy 它必须先实现 Clone 特征,因为一个类型实现 Clone 后,就等于顺便实现了 Copy 。

rust
#[derive(Copy, Clone)]  
struct Point {  
    x: i32,  
    y: i32,  
}

总之, Copy 拥有更好的性能,当浅拷贝足够的时候,就不要使用 Clone ,不然会导致你的代码运行更慢,对于性能优化来说,一个很大的方面就是减少热点路径深拷贝的发生。

其他 ​

所有权转移其实可以分为两类:栈上数据的复制和堆上数据的转移,这也是非常符合直觉的,例如i32这种类型实现了Copy特征,可以存储在栈上,因此它就是复制行为,而String类型是引用存储在栈上,底层数据存储在堆上,因此转移所有权时只需要复制一下引用即可。(Rust中基本类型的赋值操作表现出 copy 的行为,复合类型的赋值操作表现出 move 的行为,如果想让复合类型的赋值操作表现出 copy 的行为,就要显式的调用 clone)

  • 对于栈上数据(如数组),move 操作可能导致深拷贝,导致性能下降,例如下面的LargeArray:
rust
struct LargeArray {
    a: [i128; 10000],
}

结构体是一个复合类型,它内部字段的数据存在哪里,就大致决定了它存在哪里。而该结构体里面的a字段是一个数组,而不是动态数组 Vec,数组是存储在栈上的数据结构。所以 LargeArray 是存于栈上的数据结构。这种类型发生移动时,会复制所有数据,此时性能较低,可以用 Box 让其存储在堆上,从而避免复制所有数据。

  • 对于堆上数据(如Box<T>或String),move 操作只复制引用
rust
fn main() {
    // 在栈上创建一个长度为1000的数组
    let arr = [0;1000];
    // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是自动调用 Copy 直接重新深拷贝了一份数据
    let arr1 = arr;

    // arr 和 arr1 都拥有各自的栈上数组,因此不会报错
    println!("{:?}", arr.len());
    println!("{:?}", arr1.len());

    // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
    let arr = Box::new([0;1000]);
    // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
    // 所有权顺利转移给 arr1,arr 不再拥有所有权
    let arr1 = arr;
    println!("{:?}", arr1.len());
    // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
    // println!("{:?}", arr.len());
}