所有权
“所有权”代表着以下意义:
每个值在Rust中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者;
每个值在一个时间点上只有一个管理者;
当变量所在的作用域结束的时候,变量以及它代表的值将会被销毁。
移动语义
一个变量可以把它拥有的值转移给另外一个变量,称为“所有权转移”。赋值语句、函数调用、函数返回等,都有可能导致所有权转移。
1 | fn create() -> String { |
所有权转移的步骤分解如下。
main函数调用create函数。
在调用create函数的时候创建了字符串,在栈上和堆上都分配有内存。局部变量s是这些内存的所有者。
create函数返回的时候,需要将局部变量s移动到函数外面,这个过程就是简单地按字节复制memcpy。
同理,在调用consume函数的时候,需要将main函数中的局部变量转移到consume函数,这个过程也是简单地按字节复制memcpy。
当consume函数结束的时候,它并没有把内部的局部变量再转移出来,这种情况下,consume内部局部变量的生命周期就该结束了。这个局部变量s生命周期结束的时候,会自动释放它所拥有的内存,因此字符串也就被释放了。
Rust中所有权转移的重要特点是,它是所有类型的默认语义。这是许多读者一开始不习惯的地方。这里再重复一遍,请大家牢牢记住,Rust中的变量绑定操作,默认是move语义,执行了新的变量绑定后,原来的变量就不能再被使用!一定要记住!
复制语义
默认的move语义是Rust的一个重要设计,但是任何时候需要复制都去调用clone函数会显得非常烦琐。对于一些简单类型,比如整数、bool,让它们在赋值的时候默认采用复制操作会让语言更简单。
比如下面这个程序就可以正常编译通过:
1 | fn main() { |
编译器并没有阻止v1被使用,这是为什么呢?因为在Rust中有一部分“特殊照顾”的类型,其变量绑定操作是copy语义。
所谓的copy语义,是指在执行变量绑定操作的时候,v2是对v1所属数据的一份复制。v1所管理的这块内存依然存在,并未失效,而v2是新开辟了一块内存,它的内容是从v1管理的内存中复制而来的。和手动调用clone方法效果一样,let v2 = v1;等效于let v2=v1.clone();。
Rust中,在普通变量绑定、函数传参、模式匹配等场景下,凡是实现了std::marker::Copy trait的类型,都会执行copy语义。基本类型,比如数字、字符、bool等,都实现了Copy trait,因此具备copy语义。对于自定义类型,默认是没有实现Copy trait的,但是我们可以手动添上。示例如下
1 | struct Foo { |
Rust提供了一个编译器扩展derive attribute,来帮我们写这些代码,
1 | #[derive(Copy, Clone)] |
Box类型
Box类型是Rust中一种常用的指针类型。它代表“拥有所有权的指针”。
1 | struct T{ |
Box类型永远执行的是move语义,不能是copy语义。对于Rust里面的所有变量,在使用前一定要合理初始化,否则会出现编译错误。对于Box
Clone VS. Copy
Copy
Copy的全名是std::marker::Copy。请大家注意,std::marker模块里面所有的trait都是特殊的trait。目前稳定的有四个,它们是Copy、Send、Sized、Sync。这几个trait内部都没有方法,它们的唯一任务是给类型打一个“标记”,表明它符合某种约定—这些约定会影响编译器的静态检查以及代码生成。
Copy这个trait在编译器的眼里代表的是什么意思呢?简单点总结就是,如果一个类型impl了Copy trait,意味着任何时候,我们都可以通过简单的内存复制(在C语言里按字节复制memcpy)实现该类型的复制,并且不会产生任何内存安全问题。
Clone
Clone的全名是std::clone::Clone。它的完整声明如下
1 | pub trait Clone : Sized { |
对于Box类型,clone执行的是“深复制”;而对于Rc类型,clone做的事情就是把引用计数值加1。
但是有一条规则需要注意:对于实现了copy的类型,它的clone方法应该跟copy语义相容,等同于按字节复制。
析构函数
所谓“析构函数”(destructor),是与“构造函数”(constructor)相对应的概念。“构造函数”是对象被创建的时候调用的函数,“析构函数”是对象被销毁的时候调用的函数。
Rust中没有统一的“构造函数”这个语法,对象的构造是直接对每个成员进行初始化完成的,我们一般将对象的创建封装到普通静态函数中。
相对于构造函数,析构函数有更重要的作用。它会在对象消亡之前由编译器自动调用,因此特别适合承担对象销毁时释放所拥有的资源的作用
析构函数不仅可以用于管理内存资源,还能用于管理更多的其他资源,如文件、锁、socket等。
在Rust中编写“析构函数”的办法是impl std::ops::Drop。Drop trait的定义如下:
1 | trait Drop { |
Drop trait允许在对象即将消亡之时,自行调用指定代码。我们来写一个自带析构函数的类型。示例如下:
1 | use std::ops::Drop; |
执行结果
1 | construct 1 |
管理资源
除去那些错误处理的代码以后,整个逻辑实际上相当清晰:首先使用open函数打开文件,然后使用read_to_string方法读取内容,最后关闭文件,这里不需要手动关闭文件,因为在File类型的析构函数中已经自动处理好了关闭文件这件事情。
1 | use std::fs::File; |
主动析构
调用标准库中的std::mem::drop函数
1 | #[inline] |
drop函数的关键在于使用move语义把参数传进来,使得变量的所有权从调用方移动到drop函数体内,参数类型一定要是T,而不是&T或者其他引用类型。
函数体本身其实根本不重要,重要的是把变量的所有权move进入这个函数体中,函数调用结束的时候该变量的生命周期结束,变量的析构函数会自动调用,管理的内存空间也会自然释放。
对于Copy类型的变量,对它调用std::mem::drop函数是没有意义的。下面以整数类型作为示例来说明:
1 | use std::mem::drop; |
变量遮蔽(Shadowing)不会导致变量生命周期提前结束,它不等同于drop。示例如下:
1 | use std::ops::Drop; |
输出
1 | construct first variable |
这里函数调用的顺序为:先创建第一个x,再创建第二个x,退出函数的时候,先析构第二个x,再析构第一个x。由此可见,在第二个x出现的时候,虽然将第一个x遮蔽起来了,但是第一个x的生命周期并未结束,它依然存在,直到函数退出。这也说明了,虽然这两个变量绑定了同一个名字,但在编译器内部依然将它们视为两个不同的变
另外还有一个小问题需要提醒读者注意,那就是下划线这个特殊符号。请注意:如果你用下划线来绑定一个变量,那么这个变量会当场执行析构,而不是等到当前语句块结束的时候再执行。下划线是特殊符号,不是普通标识符。示例如下:
1 | use std::ops::Drop; |
输出
1 | destructor for 2 |
最后,请大家注意区分,
std::mem::drop()函数和std::ops::Drop::drop()方法。
1)
std::mem::drop()函数是一个独立的函数,不是某个类型的成员方法,它由程序员主动调用,作用是使变量的生命周期提前结束;
std::ops::Drop::drop()方法是一个trait中定义的方法,当变量的生命周期结束的时候,编译器会自动调用,手动调用是不允许的。
2)
std::mem::drop
std::ops::Drop::drop(&mut self)的参数类型是&mut Self,采用的是可变借用。在析构函数调用过程中,我们还有机会读取或者修改此对象的属性
Drop VS. Copy
要想实现Copy trait,类型必须满足一定条件。这个条件就是:如果一个类型可以使用memcpy的方式执行复制操作,且没有内存安全问题,那么它才能被允许实现Copy trait。反过来,所有满足Copy trait的类型,在需要执行move语义的时候,使用memcpy复制一份副本,不删除原件是完全不会产生安全问题的。
本节中需要强调的是,带有析构函数的类型都是不能满足Copy语义的。因为我们不能保证,对于带析构函数的类型,使用memcpy复制一个副本一定不会有内存安全问题。所以对于这种情况,编译器是直接禁止的。
报错代码
1 |
|
析构标记
首先判断一个变量是否可能会在多个不同的路径上发生析构,如果是这样,那么它会在当前函数调用栈中自动插入一个bool类型的标记,用于标记该对象的析构函数是否已经被调用,生成的代码逻辑像下面这样: