0x00 开篇
上一篇文章介绍了所有权的概念,本篇文章将再次向大家介绍两个常见的 trait —— Copy 和 Clone。在所有权的介绍过程中,我总是说在堆上保存数据的类型才会拥有所有权,其实并不是很准确。本篇文章将厘清哪些数据拥有所有权,哪些数据不需要所有权。本篇文章的阅读时间大约 8 分钟。
0x01 Clone
clone 方法是创建一个独立的副本并返回,方法的返回值类型是 Self。以 String 为例,当我们使用 clone 方法时,不仅需要复制栈上的指针,还要复制指针指向堆上的字符串。没错,可以将 clone 方法理解为是深复制,因此,调用 clone 方法是会消耗一定的时间和内存。
源码(clone.rs):
pub trait Clone: Sized { fn clone(&self) -> Self; }
Clone 是可以与 derive
属性一起使用的。我们可以为自定义的结构体来实现 Clone trait。
示例代码如下:
#[derive(Debug, Clone)] struct Student { name: String, } fn main() { let student1 = Student { name: String::from("xiaoming") }; let student2 = student1.clone(); println!("student1 = {:?}", student1); println!("student2 = {:?}", student2); // 运行结果 // student1 = Student { name: "xiaoming" } // student2 = Student { name: "xiaoming" } }
如何确定是真的发生深复制了呢?没错,让我们来看下内存。
image-20221031214143607
下断点,看内存。从上图可以看出,student1 和 student2 的地址不一样,并且 student1 和 student2 中字符串的地址也是不一样的。student1 字符串的地址是 0x000002433bbbbb30
,student1 字符串的地址是 0x000002433bbbbb50
。定位到该地址(下图)查看。
所以, Clone trait 是用于标记可以对值深复制的类型,堆上的数据和栈上的数据。其实 String 类型默认也实现了 Clone trait,所以这里需要注意,如果结构体或者枚举类型被 #[derive(Clone)]
标记,那么结构体或者枚举的每个字段也必须实现 Clone trait。
另外,在介绍所有权时,如果我们把一个变量传递到一个函数体内,那么在原作用域内这个 变量将会变为无效。其实,我们可以通过 clone 方法,来实现深复制,将深复制后的数据传递到函数体内,而原有的变量则不会失去所有权。这样的做法并不推荐,会损耗性能。
示例代码:
fn main() { // 创建 s1 let s1 = String::from("Zhang San"); // 深复制一个 s1 传递给 print_name 函数 print_name(s1.clone()); // 打印原来的 s1 println!("{}", s1); // 运行结果 // My name is Zhang San // Zhang San } fn print_name(name: String) { println!("My name is {}", name); }
0x02 Copy
在 Rust 中,对于大部分类型来讲,在赋值时都是转移值,而不是复制值。Copy trait 继承自 Clone trait。
源码(marker.rs):
pub trait Copy: Clone { // Empty. }
从源码可以看到 Copy trait 是空的,其实在 Rust 中,Copy trait 是一个标记类型。**通常被标记为 Copy trait 的这些类型在变量赋值时不会发生转移。**被标记为 Copy trait 的类型通常数据仅保存在栈上,对于这些类型进行复制(copy)操作时内存耗时少,代价也小。
Rust 标准库中,默认基本数据类型都标识了 Copy 类型,如下:
- 整数类型:
usize
、u16
、u32
、u64
、u128
、isize
、i16
、i32
、i64
、i128
- 浮点类型:
f32
、f64
- 布尔类型:
bool
- 字符类型:
char
以及其它一些 Sized 类型的共享引用(不可变引用)和原始指针等。——暂时可忽略
Copy 是可以与 derive
属性一起使用的。我们可以为自定义的结构体来实现 Copy trait。
示例代码:
fn main() { let rec1 = Rectangle { width: 10, height: 5 }; // 注意:同样需要使用 clone 方法 let rec2 = rec1.clone(); println!("rec1 的宽 : {}, 高 : {}", rec1.width, rec1.height); println!("rec2 的宽 : {}, 高 : {}", rec2.width, rec2.height); // 运行结果 // rec1 的宽 : 10, 高 : 5 // rec2 的宽 : 10, 高 : 5 }
0x03 扩展——struct 和 enum 属于什么类型?
假设,我们将含有字符串的 struct 强制标记 Copy trait,会发生什么?
编译会发生错误,如果一个 strut
或者 enum
是实现了Copy
,那么它所属的所有字段都必须实现 Copy
。因此 Copy
类型包含的类型较少 ,限制性非常大。至于 strut
或者 enum
属于什么类型,这取决于它包含的类型了。
0x04 小结
本篇文章通过所有权了解了另外两个常见的 trait ——Copy
和 Clone
。Rust 基本库为基本数据类型实现了 Copy
。通俗来讲,Clone
主要标记可以对值进行深复制的类型,而 Copy
主要标记可以进行浅复制的类型。还有要注意的一点,所有我们自定义的类型都默认属于非Copy类型。如果自定义的结构体或者枚举的所有字段都是Copy
类型,那我们需要使用 #[derive(Copy,Clone)]
标注。在各种开发场景中使用 Copy 标注时要谨慎,以免后续扩展带来不必要的麻烦。