所有权系统是Rust敢于声称自己为一门内存安全语言的底气来源,也是让Rust成为一门与众不同的语言的所在之处。也正是因为这个特别的所有权系统,才使得编译器能够提前暴露代码中的错误,并给出我们必要且精准的错误提示。
所有权系统的三个规则
- 每个值都有一个所有者,内存中不可能存在一个没有所有者的值;
- 一个值只有一个所有者, 没有变量可以共享一个值的所有权,其他变量可以借用这个值,但只有一个变量可以拥有它;
- 如果某个值的所有者超出了它的作用域,这个值也会立刻从内存中被抹去;
所有权的移动
情景A
让我们用示例来说明上面的文字:
let s1 = String::from("abc");
let s2 = s1;
println!("{}", s1); // Error!
上面的例子中, 我们创建了一个字符串变量s1, 然后创建了另一个变量s2, 并将s1的值赋给它。 此时, 在Rust内存中发生的事并不是进行了一次值拷贝, 而是把s1的值移动给了s2, s1不再有值, 因为只有一个变量可以拥有该值。如果我们在进行了上面的操作之后尝试继续使用s1, 就会出现编译器错误:
让我们从内存的角度来看看上面的代码发生了什么, 首先创建一个变量s1, 上一章讲解字符串的内容中说到了, String类型的数据结构由指针,长度, 容量三部分组成,这三部分数据被压入栈中。在堆中创建了值abc
, s1的指针指向堆中值所在的地址:
然后再创建s2, s2的指针, 长度, 容量都会从s1复制,并作为一个新的变量被压入栈中。
如果到此为止, s1和s2的指针就都指向了同一个内存地址,这样一来,内存安全就不复存在了, 因此Rust会使s1立即失效。
编译器现在会认为s1是一个已声明但是未被初始化的变量,因此是不能被使用的。如果s1被声明为一个可变的变量,理论上我们还是可以再次对它进行赋值并使用的。但是在上面的代码中,我们没有使用mut
关键字声明它为可变,因此s1始终是不可变的,他的值被移动给s2后, s1就只是一个垃圾,不能再被使用了。
如果我们不想移动s1的值,而是真的想要拷贝一份呢,那可以使用clone()
函数:
let s1 = String::from("abc");
let s2 = s1.clone();
println!("{}", s1); // Error!
clone()
函数在内存中的行为也与值的移动不同, 不仅在栈中会复制一个变量, 在堆中也会复制一份相同的数据,并调整新变量的指针指向新复制的数据地址。
在Rust中, copy
一般认为是在栈中进行的复制,clone
一般认为涉及堆数据及指针更新, 在其他语言中,可以分别对应浅拷贝和深拷贝。
当变量超出作用域时,会被立即销毁,从内存堆栈的角度看,销毁意味着三件事:
- 析构函数立即执行(如果有的话)
- 堆中的数据被立即删除
- 栈中的数据立即弹出
因此,不会存在内存泄漏,悬空指针这样的问题。
情景B
let s1 = String::from("abc");
do_stuff(s1);
println!("{}", s1); // Error! s1 的值的所有权被移动到了do_stuff的局部变量s中fn do_stuff(s: String) {// do stuff
}
上面的代码中,我们创建了一个String类型的变量s1, 然后创建了一个接受字符串参数但不返回任何内容的函数。如果我们将s1作为参数传递给该函数, s1的值的所有权将被移动到do_stuff函数中的局部变量s 中, 这就意味着s1将失去对其值的所有权,而不能再继续被使用了。那如果我们还想继续使用s1呢,可能会想到这样做:
let mut s1 = String::from("abc");
s1 = do_stuff(s1);
println!("{}", s1); fn do_stuff(s: String) -> String {s
}
让s1声明为一个可变变量, 让函数返回一个String类型的值,并重新赋值给s1。 看起来是解决了问题,但是总是感觉画蛇添足,怪怪的样子。跳出代码想一想这个问题, 通常,我们将变量传入函数,无非是想要使用这个值, 而其实使用这个值并不一定需要将值的所有权传递给函数,在下一章中,我们会讨论引用与借用,这将解决我们的这种需求。
小结
本章介绍了Rust的所有权系统的规则与示例,接下来会讲解Rust中的引用与借用。