Rust 中的内存管理跟其它语言基本类似,提供了浅复制和深复制两种方式,使用时略有区别,这里简单介绍。
简介
在 Rust 中,两个 Trait 都可以用来复制数据,区别是:
Copy
通过按位拷贝的方式进行复制,赋值、传参等场景中会调用该函数。Clone
可以通过clone()
方法进行复制,用户可以自定义,需要显示调用。
两种 trait
都可以通过 #[derive(Copy, Clone)]
自动派生,也就是编译器自动生成实现,用户可以根据使用场景决定只实现其中一个还是两个都实现。
注意,自动派生会遍历每个成员变量执行对应的 trait
实现,这也就意味着如果有些类型不支持,那么只能自己实现。
接着一步步看一些使用过程中常见的问题,已经如何更加 Rustacean 。
Copy VS. Clone
Copy
实际上是一个 Marker Trait
,只对编译器有效,不会生成函数,所以,如下两种方式是等价的。
impl Copy for Foobar {}
impl Clone for MyStruct {
fn clone(&self) -> MyStruct {
*self
}
}
#[derive(Copy, Clone)]
struct Foobar{}
进行赋值时,默认会转移所有权,但是,当实现了 Copy
特征之后,那么就会采用按位复制 (浅拷贝) 执行,所有权不会发生转移,假设有如下代码。
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let r1 = Rectangle{width: 10, height: 20};
let r2 = r1;
let r3 = r1.clone();
println!("copy {:?}, clone {:?}", r2, r3);
}
编译时会在 let r3 = r1.clone()
地方报 borrow of moved value: r1
的错误,也就是前面的 let r2 = r1
发生了所有权转移,如果要避免,可以添加 Copy trait
实现。
修改也很简答,只需要将 #[derive(Debug, Clone)]
修改为 #[derive(Debug, Clone, Copy)]
即可,此时就会使用类似 memcpy
方式复制。
这种场景下两种实现方式效果相同,每次都会复制了一份数据,各个变量之间不会发生相互干扰。
Clone
如上,这种方式需要手动调用。
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Rectangle {
id: String,
width: u32,
height: u32,
}
fn main() {
let r1 = Rectangle{id: String::from("Rectangle"), width: 10, height: 20};
let r2 = r1.clone();
println!("orignal {:?}, clone {:?}", r1.id.as_ptr(), r2.id.as_ptr());
}
此时两个变量中 id: String
指向的是堆上的不同内存。
将上述的结构体修改为 #[derive(Debug, Clone, Copy)]
,那么在 id: String
变量的地方会报 this field does not implement Copy
的错误,显然,并非所有类型都可以实现 Copy
这个 trait
。如果 String
类型允许 Copy
,那么两个变量会指向相同的栈内存,那么就可能会出现内存重复释放。
这种方式需要复制内存,效率比较低,最好通过引用、Arc 等方式。
Derive
对结构体使用自动推导需要确保所有的成员变量同样支持 Copy 接口,常见的 String Vec 是不支持的,而不可变引用支持。
其它
在 Rust 内部,Copy 被定义为如下,也就是实现 Copy 必须要同时实现 Clone 。这个依赖实际上是语义上或者实际使用场景中的最佳实践,通过 Clone 可以进行深复制,而 Copy 只复制必要内容,是对 Clone 的一种性能上优化。
所以,只有当存在成本高的深复制方式时,才有必要优化为浅复制,否则只需要通过浅复制即可。
pub trait Copy: Clone {
}
Drop
通常用于 Resource Acquisition Is Initialization, RAII 场景,一般就是智能指针场景,或者有些用户自定义的清理逻辑。
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Rectangle {
id: String,
width: u32,
height: u32,
}
impl Drop for Rectangle {
fn drop(&mut self) {
println!("Drop data {:?}", self.id.as_ptr());
}
}
fn main() {
let r1 = Rectangle{id: String::from("Rectangle"), width: 10, height: 20};
let r2 = r1.clone();
println!("orignal {:?}, clone {:?}", r1.id.as_ptr(), r2.id.as_ptr());
drop(r1);
drop(r2);
}
Rust 会按照变量声明的顺序调用清理逻辑,包括代码中声明的变量,以及结构体中的变量,不允许手动调用析构函数,不过可以通过 drop(val)
这种方式显示删除。
另外,实现了 Drop
和 Copy
是不允许同时存在的,这也很显然,通过位复制方式得到的新变量是可以直接清理的,而使用 Drop
自定义又不会用到 Copy
实现。
ManuallyDrop
一个包装器,用来禁止编译器自动调用 T
的析构函数,可以控制某些场景下的资源清理逻辑。
use std::mem::ManuallyDrop;
fn main() {
let mut val = ManuallyDrop::new(String::from("Hello World!"));
val.truncate(5); // handle value safty
println!("{:?}", val);
unsafe { ManuallyDrop::drop(&mut val) };
}
上述是通过手动方式释放资源,另外,还可以如下方式重新获取封装的对象,这样 Rust
会自动释放资源。
let v0 = unsafe { ManuallyDrop::take(&mut val) };
println!("{:?}", v0);
不过建议使用 ManuallyDrop::into_inner(val);
这种方式。另外 std::mem::forget<T>
函数实现,使用的就是 ManuallyDrop::new
方法。