Appearance
11.1 什么是所有权
拿 C 语言的代码来打个比方。我们可能会在堆上创建一个对象,然后使用一个指针来管理这个对象:
rust
Foo *p = make_object("args");
接下来,我们可能需要使用这个对象:
rust
use_object(p);
然而,这段代码之后,谁能猜得到,指针 p 指向的对象究竟发生了什么?它是否被修改过了?它还存在吗,是否已经被释放?是否有另外一个指针现在也同时指向这个对象? 我们还能继续读取、修改或者释放这个对象吗?实际上,除了去了解use_object
的内部实现之外,我们没办法回答以上问题。
对此,C++ 进行了一个改进,即通过“智能指针”来描述“所有权”(Ownership)概念。这在一定程度上减少了内存使用 bug,实现了“半自动化”的内存管理。 而 Rust 在此基础上更进一步,将“所有权”的理念直接融入到了语言之中。
Rust 有以下 3 条所有权规则:
每个值在 Rust 中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者;
每个值在一个时间点上只有一个所有者;
当变量所在的作用域结束的时候,变量以及它代表的值将会被销毁。
规则 1 给出了所有者owner的概念,规则 2 对应所有权的转移 Move 语义,规则 3 对应内存释放 Drop。 Rust通过单一所有权,将堆上内存管理与分配在栈上的所有者变量生命周期绑定,所有者离开作用域后堆内存也自动释放。
我们再拿前面已经讲过的字符串 String 类型来举例:
rust
fn main() {
let mut s = String::from("hello");
s.push_str(" world");
println!("{}", s);
}
当我们声明一个变量s
,并用 String 类型对它进行初始化的时候,这个变量s
就成了这个字符串的“所有者”。 如果我们希望修改这个变量,可以使用mut
修饰s
,然后调用 String 类型的成员方法来实现。 当main
函数结束的时候,s
将会被析构,它管理的内存(不论是堆上的,还是栈上的)则会被释放。
我们一般把变量从出生到死亡的整个阶段,叫作一个变量的“生命周期”。比如这个例子中的局部变量s
,它的生命周期就是从let
语句开始,到main
函数结束。
在上述示例的基础上,若做一点修改:
rust
fn main() {
let s = String::from("hello");
let s1 = s;
println!("{}", s);
}
编译,可见:
error[E0382]: use of moved value: `s`
--> test.rs:5:20
|
4 | let s1 = s;
| -- value moved here
5 | println!("{}", s);
| ^ value used here after move
|
= note: move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
这里出现了编译错误。编译器显示,在let s1 = s;
语句中,原本由s
拥有的字符串已经转移给了s1
这个变量。所以,后面继续使用s
是不对的。
也就是前面所说的每个值只有一个所有者。变量s
的生命周期从声明开始,到 move 给s1
就结束了。变量s1
的生命周期则是从它声明开始,到函数结束。 而字符串本身,由String::from
函数创建出来,到函数结束的时候就会销毁。中间所有权的转换,并不会将这个字符串本身重新销毁再创建。 在任意时刻,这个字符串只有一个所有者,要么是s
,要么是s1
。
请注意,Rust 的赋值和 C++ 的赋值有重大区别。如果我们用 C++ 来实现上面这个例子的话,程序如下:
rust
#include <iostream>
using namespace std;
int main() {
string s("hello");
string s1 = s;
cout << s << endl;
cout << s1 << endl;
return 0;
}
在用变量s
初始化s1
的时候,并不会造成s
的生命周期结束。这里只会调用 string 类型的复制构造函数复制出一个新的字符串,于是在后面s1
和s
都是合法变量。 在 Rust 中,我们要模拟这一行为,需要手动调用clone()
方法来完成:
rust
fn main() {
let s = String::from("hello");
let s1 = s.clone();
println!("{} {}", s, s1);
}
在 Rust 里面,不可以做“赋值运算符重载”,若需要“深复制”,必须手工调用clone
方法。 这个clone
方法来自于std::clone::Clone
这个 trait。clone
方法里面的行为是可以自定义的。