浅谈rust中的Copy与Clone Trait

Copy 与 Clone 区别

在Rust中,Copy和Clone都是用于复制(或克隆)值类型的trait。Copy trait表示这个类型可以通过按位拷贝的方式进行复制,而Clone trait则表示这个类型可以通过clone()方法进行复制。

这两个trait都可以被用于自动派生(derive)。在struct中添加#[derive(Copy, Clone)],可以让编译器自动生成实现Copy和Clone trait的代码。

通常情况下,一个类型要么实现Copy trait,要么实现Clone trait,而不是同时实现两个trait。但是,在某些情况下,同时派生Copy和Clone trait可以方便地实现这两种复制行为。

例如,在需要对一个类型进行按位复制的场景下,可以实现Copy trait,同时在需要对一个类型进行更复杂的复制逻辑的场景下,可以实现Clone trait。由于这两个trait的实现方式不同,因此在不同的场景下,它们提供的复制行为也会有所不同。

因此,在一些需要同时支持按位复制和复杂复制逻辑的场景下,同时派生Copy和Clone trait可以非常方便和灵活。

刚刚我们提到「按位的方式复制」和采用「clone()方法复制」,接下来我们再来探讨下这两种复制方法。

按位复制与clone()复制

在Rust中,按位复制(bitwise copy)是指对于一个类型的值,直接对其内存进行位复制的操作。这通常只适用于简单的值类型,例如基本数值类型和布尔类型等。具体来说,如果一个类型实现了Copy trait,则它可以被按位复制。

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

let p1 = Point { x: 10, y: 20 };
let p2 = p1; // 使用按位复制的方式将 p1 复制给 p2

println!("p1: ({}, {})", p1.x, p1.y); // 输出 p1: (10, 20)
println!("p2: ({}, {})", p2.x, p2.y); // 输出 p2: (10, 20)

相比之下,通过clone()方法进行复制则更加灵活。clone()方法是一个函数,它可以根据类型的实现进行不同的操作,以实现复制行为。通常,这种方式更适合于复杂的类型,例如包含引用、指针或其他复杂结构的类型。

基于上面的示例,我们看看clone()的使用方法

let p1 = Point { x: 10, y: 20 };
let p2 = p1.clone(); // 使用clone()方法将 p1 复制给 p2

println!("p1: ({}, {})", p1.x, p1.y); // 输出 p1: (10, 20)
println!("p2: ({}, {})", p2.x, p2.y); // 输出 p2: (10, 20)

// 当我们使用clone()方法将一个结构体的值复制到另一个结构体时,Rust 编译器会采用深度复制的方式。这意味着,它会复制整个结构体,包括其中的每一个字段,而不仅仅是复制指针或引用。因此,当我们对其中一个结构体进行修改时,另一个结构体的值不会受到影响。

// 需要注意的是,在实现Clone trait时,需要确保所有的字段都实现了Clone trait,以便正确地进行深度复制。如果某个字段没有实现Clone trait,编译器将无法自动为该结构体实现Clone trait,而需要我们手动为该字段实现Clone trait,或使用其他技术来确保正确的复制。

具体来说,通过clone()方法进行复制的实现通常会使用深拷贝(deep copy)或浅拷贝(shallow copy)的方式,具体取决于类型的实现。深拷贝会复制整个数据结构,包括其中的引用和指针等,而浅拷贝只会复制值类型,不会复制引用和指针等。

因此,按位复制和clone()方法复制之间的区别在于,按位复制只适用于简单的值类型,而clone()方法则更适合于复杂的类型。在实现类型的复制行为时,应该根据具体情况来选择使用哪种方式。

最后,我们再来稍微深入点看看两种复制方式在内存中的表现形态。

内存简单示意图

只在栈上复制

假设我们有一个包含两个i32类型字段的Point结构体,并且实现了Copy和Clone trait。现在,我们创建了一个Point类型的变量p1,它的值为{x: 10, y: 20}。然后,我们将p1复制给p2,分别使用按位复制和clone()方法复制两种方式,来比较它们的内存布局。

按位复制的示意图:

+--------+       +--------+
|   p1   |       |   p2   |
+--------+       +--------+
|   10   |-----> |   10  |
+--------+       +--------+
|   20   |       |   20  |
+--------+       +--------+

我们可以看出,在按位复制的方式下,p1和p2占用不同的内存空间,但它们的值完全相同,因为编译器会将p1的值按位复制到p2中。

接下来,我们再看看clone()方法复制的示意图:

+--------+       +--------+
|   p1   |       |   p2   |
+--------+       +--------+
|   10   |-----> |   10   |
+--------+       +--------+
|   20   |       |   20   |
+--------+       +--------+

我们不难发现,由于 i32 类型是基本类型,可以进行按位复制,因此在栈空间中直接复制两个变量的值即可。在使用 clone() 方法复制时,由于 i32 类型是 Copy 的,所以也会直接进行按位复制,从而得到相同的内存布局。因此,在栈空间中按位复制和 clone() 方法复制的内存布局是相同的。

可能在栈,可能在堆上复制

再来给出一个简单的实例进行说明,假设我们有一个Person struct,包含一个姓名(String 类)和一个年龄(无符号32位整型)属性,然后我们分别对其进行「按位复制」和「clone()复制」

#[derive(Clone, Copy)]
struct Person {
    name: String,
    age: u32,
}

fn main() {
    let person1 = Person { name: String::from("Alice"), age: 30 };

    // 按位复制
    let person2 = person1;

    // clone()方法复制
    let person3 = person1.clone();
}

根据Person结构体的定义,name字段是一个String类型,它是在堆空间上分配内存的,而age字段是一个u32类型,它是在栈空间上分配内存的。因此,示意图中的区别涉及到的是堆空间的内存分配。

在按位复制的情况下,由于Person结构体实现了Copy trait,因此整个结构体的值可以被直接复制,包括它在堆空间上的name字段。因此,示意图中的两个结构体实例(person1person2)都包含了指向堆空间上同一块内存的name字段。

clone()方法复制的情况下,name字段的值不会被直接复制。相反,clone()方法会为name字段分配一块新的内存,并将原始String值的副本存储在其中。因此,示意图中的两个结构体实例(person1person3)包含了指向不同的堆空间内存的name字段。

再来通过示意图直观观察下两者在内存中的区别:

按位复制:
+--------+       +--------+
| person1|       | person2|
+--------+       +--------+
|  name  |----|   name  |----------
+--------+    |   +--------+      |
|  age   |    |   |  age   |      |
+--------+    |   +--------+      |
              |                   |
              -----            |---
                  |            | 
堆空间:           |     --------
                  V    V
+--------+      +---------+  
|  name  |----> | "Alice" | 
+--------+      +---------+  

clone()方法复制:
+--------+       +--------+
| person1|       | person3|
+--------+       +--------+
|  name  |----   |   name  |------
+--------+    |  +--------+      |
|  age   |    |  |  age   |      |
+--------+    |  +--------+      |
              |                  |
              -----              |
                  |              | 
堆空间:           |              |     
                  V              V
+--------+      +---------+   +---------+  
|  name  |----> | "Alice" |   | "Alice" | 
+--------+      +---------+   +---------+  
                   旧空间         新空间

最后,我们再看看包含引用类型的结构体 Person重载clone()的情况

struct Person {
    name: String,
    age: u32,
}

impl Clone for Person {
    fn clone(&self) -> Person {
        Person {
            name: self.name.clone(),
            age: self.age,
        }
    }
}

impl Copy for Person {}

现在,我们创建了一个 Person 类型的变量 p1,它的 name 字段是一个 String 类型的引用,值为 "Alice"。然后,我们将 p1 复制给 p2,分别使用按位复制和 clone() 方法复制两种方式,来比较它们的内存布局。

假设 p1p2 都是在栈空间分配内存,那么按位复制的示意图如下:

Stack:

   p1                p2
┌──────────┐      ┌──────────┐
│ name ptr ├───┐  │ name ptr │
├──────────┤   │  ├──────────┤
│ age      │   │  │ age      │
└──────────┘   │  └──────────┘
               │
               │
 Heap:         │
               │
   name ptr ───┘
┌──────────┐
│ "Alice"  │
└──────────┘

可以看出,按位复制只是简单地复制了 name 字段的地址,而没有复制它所指向的堆内存。因此,p1 和 p2 共享同一个 name 字段所在的堆内存,当其中一个变量修改 name 字段时,会影响到另一个变量。

而使用 clone() 方法复制的示意图如下:

Stack:

   p1                p2
┌──────────┐      ┌──────────┐
│ name ptr ├───┐  │ name ptr ├───┐
├──────────┤   │  ├──────────┤   │
│ age      │   │  │ age      │   │
└──────────┘   │  └──────────┘   │
               │                 │
               │                 │
 Heap:         │                 │
               │                 │
   name ptr ───┘        ┌──────────┐
                        │ "Alice"  │
                        └──────────┘

可以看出,clone() 方法将复制 name 字段所指向的堆内存,因此 p1 和 p2 拥有各自独立的 name 字段。这样,当其中一个变量修改 name 字段时,不会影响到另一个变量。

本作品采用《CC 协议》,转载必须注明作者和本文链接
努力是不会骗人的!
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!