Rust Tips No.3 Copy, Clone, Drop

263 阅读7分钟

当我们说移动的时候我们在说什么?

Rust有一个很关键的概念,就是移动语意(move semantics),相对应的还有一个概念是拷贝语意(copy semantics)。

这里面其实再细分,可以分成三个概念,move,copy和semantics。

我们先来看move和copy,我们先来看代码:


// code1

let mut x1:u32 = 1;

x1 = 2;

let mut x2 = x1;

x2 = 3;

  


// code2

struct A(u32);

let mut a1 = A(10);

a1.0 = 11;

let mut a2 = a1;

a2.0 = 12;

  


// code3

#[derive(Clone, Copy)]

struct B(u32);

let mut b1 = B(20);

b1.0 = 21;

let mut b2 = b1;

b2.0 = 22;

以上三段代码,生成的汇编代码是一样的,再看一段复杂一点的:


// code1

struct AA([u8; 256]);

let mut aa1 = AA([0; 256]);

aa1.0[0] = 100;

let mut aa2 = aa1;

aa2.0[0] = 101;

  


// code2

#[derive(Clone, Copy)]

struct BB([u8; 256]);

let mut bb1 = BB([0; 256]);

bb1.0[0] = 200;

let mut bb2 = bb1;

bb2.0[0] = 201;

以上两段代码,生成的汇编代码也是一样的,在a2=a1的时候会调用memcpy,把a1的内存拷贝一份到a2中去。

也就是说,无论是移动语意(move semantics),还是拷贝语意(copy semantics),都会把=右边的内存拷贝一份到左边来。

那么它们的区别是什么?

答案在语意semantics这个概念上。

当上面这段代码之后,下面这段代码就有巨大的区别:


// aa1.0[0] = 0; // 不可以

bb1.0[0] = 0; // 可以

也就是说虽然在汇编角度代码是一样的,但是在语意角度,它们是不同的,对于AA和BB两种类型来说,它们在语意上的差别就在于Copy Trait。

Copy Trait

我们先来看Copy Trait的定义。


pub trait Copy: Clone {

// Empty.

}

是的,又是空的,它只是一个开关。如果我们给每个变量加一个标志代表这个变量是活着还是死亡的话,前面的代码可以这样理解:


// aa1 活着

let aa2 = aa1;

// aa1 死亡,aa2 活着

  


// bb1 活着

let bb2 = bb1;

// bb1 活着 bb2 活着

对于已经死亡的变量,Rust不允许任何访问,这就是aa1移动所有权给aa2后再访问会报错的原因。

那么一个类型的Copy开关,代表的语意是:

  • 当一个类型没有Copy Trait,也就是Copy开关关闭的时候,=右边的变量会标记为死亡

  • 当一个类型impl了Copy Trait,也就是Copy开关打开的时候,=右边的变量不会有变化,不会死亡。

这就是Copy Trait在语意上的意义。

从编译器的角度,所有的类型,默认的情况下,Copy开关都是关闭的,请注意,是所有。

也就是说,所有的赋值=操作,执行的都是移动语意,会给=右边的变量标记为死亡。

如果你希望右值不死亡,那么,需要给该变量的类型,实现Copy,impl Copy for 你要打开的Copy的类型。

在标准库中,Rust默认给所有的基本类型,*const指针 *mut指针,和&T引用,都impl了Copy。

这里特别要注意的是&mut T,没有实现Copy,因为可变引用必须,只能是移动语意。

学习Rust常听到的可变引用只能有一个,不可变引用可以有多个,这条原则在core库中的体现,就是没有impl Copy for &mut T 这行代码。

那么我们刚才说的一个变量的死亡,又是什么意思呢?

Drop

下面这个Trait定义在core::ops mod中:


pub trait Drop {

fn drop(&mut self);

}

定义在core::ops mod中的Trait都是用来做操作符重载的。

Drop Trait的意义就是,当一个变量死亡,在离开作用域的时候,会自动调用相应的drop函数。

也可以理解为两个Trait,一个管杀,一个管埋,Copy管杀,Drop管埋。

这里需要注意的是作用域这个概念,形象的理解就是那一对大括号{}。右边这个大括号"}",就是埋变量的地方,不光Copy杀掉的变量,所有在作用域里面的变量,只要不是返回的,都会在这里一起被杀掉,统一一起埋掉。

也就是说当我们调用=移动一个变量的时候,它只是标记为死亡了,没有埋掉,drop函数不会调用,所有的drop函数都在"}"埋的这个地方统一一起调用。

在core::mem中,还定义了另外一个函数:


pub fn drop<T>(_x: T) {}

这个函数的作用就是给我任何变量,我给你连杀带埋一条龙服务全处理掉。这个函数主要用在一些我们需要明确提前调用变量类型的drop(&mut self)函数的地方。比如我们打开了一个文件,数据读完,处理的过程比较长,我们要先把文件关掉,然后在慢慢处理数据,这时候就可以在读完数据后,drop(file),提前执行file的drop(&mut self)函数关闭文件。

Clone

在Copy的定义中还涉及到一个Trait,就是Clone,该Trait定义在core::clone mod 中:


pub trait Clone: Sized {

fn clone(&self) -> Self;

fn clone_from(&mut self, source: &Self) {

*self = source.clone()

}

}

这个Trait的作用是定义,如何克隆一个一摸一样内容的新实例出来。注意这个Trait是定义在单独的clone mod中的,也就是说这个东西跟生命周期,移动语意等等没有任何关系,它的作用很单纯,就是我们需要一个一摸一样的新实例的时候,这个Trait可以帮我们生成一个。

当我们给一个类型定义Copy以后,我们使用=,使用copy semantics来得到一个新的一摸一样的变量,但是如何建立这个新变量呢?

单纯的memcpy一份肯定是不现实的,每个类型都要有不同的实现才行,Clone干的就是给每一个类型定义这个克隆的过程。

Copy并不管构建新变量的,具体来执行这件事的是Clone,这也是为什么Copy的定义中需要 :Clone 的原因。

与Copy一样,默认的情况下,所有的类型,都是没有实现Clone的,core标准库为所有的基础类型,*const,*mut ,和&T都实现了Clone。

额外的,还有这一行:


impl<T: ?Sized> !Clone for &mut T {}

注意这里的 !符号,意思就是明确说明&mut T是没有实现Clone的,目的与Copy一样,可变引用只能有一个。

derive

与Sized相反,Rust语言本身的概念里,任何类型都是没有impl Copy,impl Clone的。

所有需要copy semantics 的类型都需要实现Clone, Copy这两个Trait才行。

但是很多的类型是需要实现这两个Trait的,标准库中的类型可以由标准库来帮助我们实现,但是我们的自定义类型呢?

如果每个类型都要实现一遍,就会很麻烦,特别是Clone,如果你的Struct有100个字段,那么你需要写100次clone来实现clone函数。

为了避免这个麻烦,标准库提供了两个属性宏,为我们自动实现。


#[derive(Clone, Copy)]

struct A(u32);

// 等于下面的代码

struct A(u32);

impl Copy for A;

impl Clone for A {

fn clone(&self) -> Self{

Self(self.0.clone())

}

}

类似的Trait还有Debug, Default,PartialEq, Eq, Hash等等,都提供了自动实现的属性宏。

总结

Copy负责改变赋值=的行为,Clone负责克隆一摸一样的变量,Drop负责销毁变量。

我想表达的是这里面的设计思路:

  1. 零成本抽象。

  2. 清晰的Trait定义,每一个Trait负责而且只负责一个具体的事情,没有交叉。

  3. 通过Trait之间的组合来实现目的。比如Copy和Clone的组合。

这样的设计不止在标准库中可以用,在应用的开发中其实更需要,而且最核心的是,语言本身的设计就是为了实现这样的套路而设计的,沿着语言的设计去去设计,去构架,去写代码,就会很顺很清晰,不会被语言本身绊倒。