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
评论区
写评论还没有评论