【Rust 中级教程】 11 所有权与trait(4)

188 阅读4分钟

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 标注时要谨慎,以免后续扩展带来不必要的麻烦。