共计 2685 个字符,预计需要花费 7 分钟才能阅读完成。
Rust 中的内存平安 — 2
原文:https://hashrust.com/blog/memory-safety-in-rust-part-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