前言
我们接下来要探讨的概念是Rust的内存安全性的核心原则及其零成本抽象原则,这写原则使Rust能够在编译时,检测到程序中的内存安全违规行为,并在相应的作用域结束时自动释放资源,等等。我们称这些概念为所有权、借用和生命周期(ownership, borrowing, and lifetimes)。在这三者中,以所有权为核心原则,而借用和生命周期是Rust类型系统的扩展,在代码的不同上下文中调整所有权原则,可以确保编译时内存管理。下面,就让我们来详细阐述一下这些概念。
程序中资源的真正所有者(a true owner of a resource)的概念因语言而异。在这里,我们可以统称为保存堆或栈上的任何变量,或者保存打开的文件描述符、数据库连接套接字、网络套接字等类似内容的变量。从它们存在的那一刻起,直到被程序使用完毕,都会占用一些内存。作为资源所有者,其一项重要的职责,就是在合适的场合释放使用的内存,否则很可能会导致内存泄漏(memory leaks)。
在用动态语言(如Python)中编程时,一个列表对象有多个所有者或别名是很常见的,开发者可以使用指向该对象的多个变量之一来添加或删除列表项,而不需要关心释放对象所使用的内存,因为垃圾采集器GC会对此负责,一旦对对象的所有引用都消失了,GC就会释放内存。
对于像C/ C++这样的编译性语言,在智能指针出现之前,有关代码在资源使用完成后,是由被调用者还是调用者负责释放内存,存在争议。这些观点之所以存在,是因为在这些语言中,编译器并没有强制所有权。在C++中因为不使用智能指针而出错的情况很多,比如,当有多个变量指向堆上的一个值是完全没问题的(尽管不建议这样做),这被称为混叠(aliasing)。一个资源使用多个指针或别名的灵活性,固然很有灵活性,处理不好的话,也会给程序员会造成各种不良影响,其中之一就是C++中的迭代器失效问题,前面已经解释过了。具体来说,当给定范围内的其他不可变(绑定)别名中至少有一个资源的可变(绑定)别名时,就会出现问题。
铺陈许久,引出我们的主角,Rust,为程序中的值的所有权,尽可能的设置适当语义。原则如下:
- 当使用let语句创建一个值或资源并将其分配给一个变量时,该变量将成为资源的所有者
- 当值从一个变量重新分配给另一个变量时,该值的所有权转移到另一个变量,旧变量失效,不可以再供进一步使用
- 值和变量在作用域结束时被重新分配
结论是Rust中的值只有一个所有者,即创建它们的变量。这个原理很简单,但其含义着实让来自某些其他语言的程序员感到过惊讶。考虑下面的代码,其以最基本的形式演示了所有权原则:
// ownership_basics.rs
#[derive(Debug)]
struct Foo(u32);
fn main() {
let foo = Foo(2048);
let bar = foo;
println!("Foo is {:?}", foo);
println!("Bar is {:?}", bar);
}
看下代码,我们创建了两个变量,foo和bar,它们指向一个foo实例。对于熟悉类似Python这种允许一仆多主的语言的人而言,习惯上希望这个程序能够很好的编译。但是在Rust中编译时,会得到以下错误
这里解释几句,我们创建了一个Foo实例,并将它分配给Foo变量。根据所有权规则,foo现在是foo实例的所有者。在下一行中,又将foo赋值给bar。在执行main的第2行时,bar成为Foo实例的新所有者,旧的Foo现在是一个被放弃的变量,已经不能在任何地方使用,所以在println!宏语句中就过不去了。
当我们将某个变量赋值给其他变量或从该新变量中进行读取时,Rust就会改变原来的所有权。所有权规则禁止使用多个访问点来进行值修改,这会使得以往在C/C++语言中可以侥幸用一用的脏活儿,不再有打擦边球的可能。比如之前提及的C++中的迭代器失效问题,可以在编译阶段直接规避。
结语
以上介绍了所有权的基本内容,但尚未完,所有权规则还考虑了变量的作用域。那么,为了分析一个值何时超出作用域,请看下篇分解。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran