Rust | Clone vs Copy
Copy的含义
Copy
的全名是std::marker::Copy
std::marker
模块里面所有的trait
都是特殊的trait
它们是跟编译器密切绑定的,
impl
这些trait
对编译器的行为有重要影响。在编译器眼里,它们与其他的trait
不一样。这几个trait
内部都没有方法,它们的唯一任务是给类型打一个“标记”,表明它符合某种约定——这些约定会影响编译器的静态检查以及代码生成
那么,Copy
这个trait
在编译器的眼里代表的是什么意思呢?
简单点总结就是,如果一个类型impl
了Copy trait
,意味着任何时候,我们都可以通过简单的内存复制(在C语言里按字节复制memcpy
)实现该类型的复制,并且不会产生任何内存安全问题。
一旦一个类型实现了Copy trait
,那么它在变量绑定、函数参数传递、函数返回值传递等场景下,都是copy
语义,而不再是默认的move
语义。
最简单的赋值语句x = y
来说明move
语义和copy
语义的根本区别
move
语义是“剪切、粘贴”操作,变量y
把所有权递交给了x
之后,y
就彻底失效了,后面继续使用y
就会出编译错误copy
语义是“复制、粘贴”操作,变量y
把所有权递交给了x
之后,它自己还留了一个副本,在这句赋值语句之后,x
和y
依然都可以继续使用
在Rust
里,move
语义和copy
语义具体执行的操作,是不允许由程序员自定义的。
move
语义或者copy
语义都是执行的memcpy
,无法更改,这个过程中绝对不存在其他副作用。
如果考虑后端优化,在许多情况下,不必要的内存复制实际上已经彻底优化掉了,不必担心执行效率的问题。没有必要每次都把move
或者copy
操作与具体的汇编代码联系起来,因为场景不同,优化结果不同,生成的代码也是不同的。只需记住的是语义。
Copy的实现条件
并不是所有的类型都可以实现Copy trait
。Rust
规定,对于自定义类型,只有所有成员都实现了Copy trait
,这个类型才有资格实现Copy trait
。
常见的数字类型、
bool
类型、共享借用指针&
,都是具有Copy
属性的类型Box
、Vec
、可写借用指针&mut
等类型都是不具备Copy
属性的类型对于数组类型,如果内部的元素类型是
Copy
,那么这个数组也是Copy
类型对于元组
tuple
类型,如果每一个元素都是Copy
类型,那么这个tuple
也是Copy
类型struct
和enum
类型不会自动实现Copy trait
。只有当struct
和enum
内部的每个元素都是Copy
类型时,编译器才允许我们针对此类型实现Copy trait
;
编译错误。原因是在
let t2 = t1
;这条语句中执行的是move
语义。但是可以手动为它impl Copy trait
,这样它就具备了copy
语义
Rust
中只有POD
(C++
语言中的Plain Old Data
)类型才有资格实现Copy trait
。
在Rust
中,如果一个类型只包含POD
数据类型的成员,并且没有自定义析构函数,那它就是POD
类型。
比如:整数、浮点数、只包含POD
类型的数组等,都属于POD
类型;而Box
String
Vec
等不能按字节复制的类型,都不属于POD
类型。
并不是所有满足POD
的类型都应该实现Copy trait
,是否实现Copy
取决于业务需求。
Clone的含义
Clone
的全名是std::clone::Clone
两个关联方法,分别是clone_from
和clone
clone_from
是有默认实现的,依赖于clone
方法的实现clone
方法没有默认实现,需要手动实现
clone
方法一般用于“基于语义的复制”操作。所以,跟具体类型的作用息息相关。比如,对于Box
类型,clone
执行的是“深复制”;而对于Rc
类型,clone
做的事情就是把引用计数值加1。
虽然Rust
中的clone
方法一般是用来执行复制操作的,但是如果在自定义的clone
函数中做点别的什么工作,编译器也没办法禁止。可以根据需要在clone
函数中编写任意的逻辑。
对于实现了
copy
的类型,它的clone
方法应该跟copy
语义相容,等同于按字节复制
自动derive
绝大多数情况下,实现Copy
Clone
这样的trait
都是一个重复而无聊的工作。因此,Rust
提供了一个attribute
,可以利用编译器自动生成这部分代码。示例如下:
;
derive
会让编译器自动生成impl Copy
和impl Clone
这样的代码。自动生成的clone
方法,会依次调用每个成员的clone
方法。
通过derive
方式自动实现Copy
和手工实现Copy
有微小的区别。当类型具有泛型参数的时候,比如struct MyStruct<T>{}
,通过derive
自动生成的代码会自动添加一个T:Copy
的约束。
总结
Copy
内部没有方法,Clone
内部有两个方法Copy trait
是给编译器用的,告诉编译器这个类型默认采用copy
语义,而不是move
语义。Clone trait
是给程序员用的,我们必须手动调用clone
方法,它才能发挥作用Copy trait
不是想实现就能实现的,它对类型是有要求的,有些类型不可能impl Copy
。而Clone trait
则没有什么前提条件,任何类型都可以实现(unsized
类型除外,因为无法使用unsized
类型作为返回值)Copy trait
规定了这个类型在执行变量绑定、函数参数传递、函数返回等场景下的操作方式。即这个类型在这种场景下,必然执行的是“简单内存复制”操作,这是由编译器保证的,程序员无法控制。Clone trait
里面的clone
方法究竟会执行什么操作,则是取决于程序员自己写的逻辑。一般情况下,clone
方法应该执行一个“深复制”操作,但这不是强制性的,如果你愿意,在里面启动一个人工智能程序都是有可能的- 如果确实不需要
Clone trait
执行其他自定义操作(绝大多数情况都是这样),编译器提供了一个工具,我们可以在一个类型上添加#[derive(Clone)]
,来让编译器自动生成那些重复的代码。编译器自动生成的clone
方法非常机械,就是依次调用每个成员的clone
方法 Rust
语言规定了在T: Copy
的情况下,Clone trait
代表的含义。即:当某变量t: T
符合T: Copy
时,它调用t.clone()
方法的含义必须等同于“简单内存复制”。也就是说,clone
的行为必须等同于let x = std::ptr::read(&t);
,也等同于let x = t;
。当T: Copy
时,不要在Clone trait
里面乱写逻辑。所以,当需要指定一个类型是Copy
的时候,最好使用#[derive(Copy,Clone)]
方式,避免手动实现Clone
导致错误。
- 深入浅出Rust