Rust 内存安全指南
Rust 中的内存安全 — 2
译者:韩玄亮(一个热爱开源,喜欢 rust 的 go 开发者)
介绍
在「rust 中的内存安全 — 1」中,讨论了内存安全性的概念以及不同语言实现内存安全的各种技术。几乎所有的语言都只聚焦一个方面上,要么是内存安全,要么是程序员控制。而 Rust 的独特之处就在于它不会做出这种取舍 —— 程序员可以同时获得内存安全和控制。
📒:
不是所有可以用 C++ 编写的程序都可以用 Safe Rust 编写。正如马上要看到的,在 Rust 中不可能出现不可控的别名,这你可以放心。Rust 在默认模式下是内存安全,但如果开发者真的想拥有 C++ 风格那样不受约束的控制,他们可以使用 Unsafe code。
别名/可变性/安全
要安全地释放一个对象,那销毁时必须没有对它的引用,否则最终将得到一个悬空指针。
类似地,如果一个线程想要将一个对象发送给另一个线程,那么发送线程上不能有对它的引用。这里有两个因素:别名和可变性。如果对象没有被销毁或通过线程发送,那么引用它并没有什么问题。只有当两者结合时,你才会遇到麻烦。
根据这一观察结果,Rust 解决内存安全的方法是:简单地同时禁止别名和可变,而 Rust 是通过所有权和借用来实现这一点。
所有权
- 当您在 Rust 中创建一个新对象时,被赋值变量成为该对象的所有者。
例如在下面的 Rust 代码中,变量 v 拥有 Vec 实例:
let v: Vec<i32> = Vec::new();
当 v 超出可表达范围时,Vec 被丢弃。
一个对象在同一时间只能有一个所有者,这确保只有所有者才能删除该对象。这避免了重复释放 (double-free) bug。如果 v 被赋值给另一个变量,则所有权转移 (v → v1):
let v1 = v; // v1 is the new owner
因为 v1 现在是所有者,所以不再允许通过 v 访问:
v.len(); // error: Use of moved value
📒:虽然 c++ 也有 move 语义,但它不能防止你引入一个 move 后使用的 bug。
- 所有者当然可以改变对象:
let mut v =Vec::new(); // mut is needed to mutate the object
v.push(1);
但是因为没有别名,所以问题不大。
不过如果开发者在 Rust 中所能做的就拥有值并传递它们,这将是一个相当受限的编程体验。幸运的是,Rust 允许从所有者那里 借用。
借用
借用引入了别名。我们可以使用 引用:从所有者那里借来:
let v:Vec<i32> = Vec::new();
let v1 = &v; // v1 has borrowed from v
v.len(); // fine
v1.len(); // also fine
与所有者不同,可以同时存在多个借用的引用:
let v:Vec<i32> = Vec::new();
let v1 = &v; // v1 has borrowed from v
let v2 = &v; // v2 has also borrowed from v
v.len(); // allowed
v1.len(); // also allowed
v2.len(); // also allowed
但是在所有者销毁后,借用者不能再访问所有者指向的内存区域数据,否则会导致一个bug(use-after-free)。
let v1: &Vec<i32>;
{
let v =Vec::new();
v1 = &v;
} // v is dropped here
v1.len(); // error:borrowed value does not live long enough
因此,即使可能存在别名,Rust 也会确保引用的生命周期不会超过被引用的对象,从而再次避免了别名和可变带来的 bug。
到目前为止,所有的借用都是不可变的。不过可变引用一定会在程序中出现,但正如接下来要看到的,Rust 足够聪明,在引入可变性的同时是不允许出现别名。
可变借用
- 虽然可以有多个共享引用,但一次只能有一个可变引用:
let mut v:Vec<i32> = Vec::new();
let v1 = &mut v; // 第一个可变借用
let v2 = &mut v; // 第二个可变借用
v1.push(1); // error:cannot borrow `v` as mutable more than once at a time
- 在允许可变引用进行变量可变时,Rust 就通过禁止其他引用(共享的或可变的)来消除别名。
这些借用规则防止悬空指针的出现。如果 Rust 同时允许可变引用和不可变引用,那么内存可能通过可变引用变得无效,而不可变引用仍然指向那个无效的内存。
例如,在下面的代码中,如果允许这样的代码通过,v1 就可以访问无效的内存:
let mut v = vec![0, 1, 2, 3]; // 可变所有者
let v1 = &v[0]; // 不可变借用
v.push(4); // Vec内部指向的内存区域发生改变,之前的缓冲区无效
let v2 = *v1; // error: 访问无效内存区域
但是,相比之下类似的代码在 c++ 中是允许编译成功的。
生命周期
上面我们已经讨论过 Rust 不允许同时使用别名和可变以防止内存安全问题,但在这几节中我一直在讨论 Rust 是如何在编译时实现这一内存安全目标。而 Rust 是怎么实现的呢?
Rust 通过跟踪变量的生命周期来实现这一点。直观地说,变量的生命周期与其作用域有关。
let v1: &Vec<i32>;//-------------------------+
{// |
let v =Vec::new(); //-----+ |v1's lifetime
v1 = &v;// | v's lifetime |
}//<-------------------------+ |
v1.len();//<---------------------------------+
所以编译器会比较各种变量的生存期,以确定是否发生了什么可疑的事情。
例如,在上面的代码中,v1 的寿命超过了所有者 v,这是不允许的。上面示例中的生存期称为词法生存期,因为它们是由变量作用域推断出来的。实际上,Rust 有一个更复杂的生命期实现,叫做 非词法生命期。
生命周期是一个很大的话题,我不可能在这篇文章中涵盖所有的内容。你可以在 Rustonomicon 中了解更多关于生命周期的信息。
总结
在这篇文章中,我们讨论了所有权和借用的概念,以及它们如何帮助实现 Rust 的内存安全。许多内存安全问题归结为一个事实,即语言本身同时允许可变和别名,比如C++。
Rust 在编译期能检测这些内存安全问题的能力使其成为系统编程语言的有力竞争者。
更多 Rust 相关内容,欢迎订阅公众号:Databend
如果想了解更多,可以关注公众号: Databend