浅谈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
字段。因此,示意图中的两个结构体实例(person1
和person2
)都包含了指向堆空间上同一块内存的name
字段。
在clone()
方法复制的情况下,name
字段的值不会被直接复制。相反,clone()
方法会为name
字段分配一块新的内存,并将原始String
值的副本存储在其中。因此,示意图中的两个结构体实例(person1
和person3
)包含了指向不同的堆空间内存的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()
方法复制两种方式,来比较它们的内存布局。
假设 p1
和 p2
都是在栈空间分配内存,那么按位复制的示意图如下:
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 协议》,转载必须注明作者和本文链接