Rust 内存管理

2021-09-18 language rust

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) 这种方式显示删除。

另外,实现了 DropCopy 是不允许同时存在的,这也很显然,通过位复制方式得到的新变量是可以直接清理的,而使用 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 方法。