简介
在上一篇文章中,我讨论了内存安全性的概念以及不同语言实现内存管理的不同技术。几乎所有的语言都只能属于一个范围,要么是语言运行时进行内存管理,保证内存安全;要么是程序员控制内存,不能保证内存安全。Rust的独特之处在于它没有进行这种权衡——程序员可以同时获得内存安全和内存控制。
别名、可变和安全性
要安全地释放一个对象,就必须没有对它的引用,否则就会出现一个悬垂指针。类似地,如果一个线程想把一个对象发送给另一个线程,在发送线程上就不能有对它的引用。这里有两个元素:别名和可变。 如果对象没有被销毁或通过线程发送,那么引用它并没有什么问题,只有当两者结合在一起时,你才会遇到麻烦。
根据这个观察,Rust的内存安全解决方案是简单的,同时不允许别名和可变,Rust通过所有权和借用实现了这一点。
所有权
当你在Rust中创建一个新对象时,被赋值的变量成为对象的所有者。例如在下面的Rust代码中,变量v拥有Vec实例:
let v: Vec<i32> = Vec::new();
当v超出范围时,Vec就会被丢弃。一次只能有一个对象的所有者,这可以确保只有所有者才能释放它。这避免了重复释放内存的错误。如果v被赋值给另一个变量,所有权转移:
let v1 = v;//v1 is the new owner
因为v1现在是所有者,所以不再允许通过v访问:
v.len();//error: Use of moved value
所有者当然可以改变对象:
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:
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足够聪明,它不允许不可变借用与可变借用混淆使用。
可变借用
虽然可以有多个共享引用,但在同一时间只能有一个可变引用:
let mut v:Vec<i32> = Vec::new();
let v1 = &mut v;//first mutable reference
let v2 = &mut v;//second mutable reference
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];//an immutable reference to Vec's first element
v.push(4);//this can invalidate Vec's internal buffer
let v2 = *v1;//this could access invalid memory
相比较下,在C++中这种相似的代码是被允许的。
生命周期
Rust通过跟踪变量的生命周期来做到以上几点,变量的生命周期与其作用域绑定:
let v1: &Vec<i32>;//-------------------------+
{// |
let v = Vec::new();//-----+ |v1's lifetime
v1 = &v;// | v's lifetime |
}//<-------------------------+ |
v1.len();//<---------------------------------+
因此,编译器比较各种变量的生存期,以找出是否有可疑的事情发生。例如,在上面的代码中,v1比所有者v活得更久,这是不允许的。上面例子中的生存期称为词法生存期,因为它们是从变量作用域推断出来的。实际上,Rust有一个更复杂的生命周期实现,称为非词汇生命周期。
总结
在这篇文章中,我讨论了所有权和借用的概念,以及它们如何帮助Rust实现内存安全。许多内存安全问题可以归结为这样一个事实:像c++这样的语言允许别名和可变同时存在。而Rust在编译时检测这些内存安全问题,这种能力使它成为系统编程语言的有力竞争者。
本文翻译自:
https://hashrust.com/blog/memory-safety-in-rust-part-2/