当我们说移动的时候我们在说什么?
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负责销毁变量。
我想表达的是这里面的设计思路:
-
零成本抽象。
-
清晰的Trait定义,每一个Trait负责而且只负责一个具体的事情,没有交叉。
-
通过Trait之间的组合来实现目的。比如Copy和Clone的组合。
这样的设计不止在标准库中可以用,在应用的开发中其实更需要,而且最核心的是,语言本身的设计就是为了实现这样的套路而设计的,沿着语言的设计去去设计,去构架,去写代码,就会很顺很清晰,不会被语言本身绊倒。