所有权和生命周期
所有权是 Rust 的突破性功能。它使 Rust 能够在避免垃圾收集的同时,实现完全的内存安全和高效。在详细介绍所有权系统之前,我们将考虑这种设计的动机。
我们假设您接受垃圾收集 (GC) 并不总是一种最佳解决方案,并且在某些情况下需要手动管理内存。如果您不接受这一点,我可以向您推荐其他语言吗?
无论您对 GC 的感受如何,它显然是使代码安全的巨大福音。您永远不必担心事物消失得太快(尽管您是否还想指向该事物是另一个问题……)。这是一个普遍存在的问题,使用非 GC 语言的程序员都需要处理。考虑一下我们所有使用过非 GC 语言的人都犯过的一个简单错误
#![allow(unused)] fn main() { fn as_str(data: &u32) -> &str { // compute the string let s = format!("{}", data); // OH NO! We returned a reference to something that // exists only in this function! // Dangling pointer! Use after free! Alas! // (this does not compile in Rust) &s } }
这正是 Rust 的所有权系统旨在解决的问题。Rust 知道 &s
的生存范围,因此可以防止它逃逸。然而,这是一个简单的例子,即使是 C 编译器也可能捕捉到。随着代码规模的扩大和指针在各种函数之间的传递,事情会变得更加复杂。最终,C 编译器将无法执行足够的逃逸分析来证明您的代码不健全。因此,它将被迫接受您的程序,并假设它是正确的。
这种情况永远不会发生在 Rust 身上。程序员有责任向编译器证明一切都是健全的。
当然,Rust 关于所有权的故事远比仅仅验证引用不会逃逸其所指对象的范围复杂得多。这是因为确保指针始终有效比这复杂得多。例如,在这段代码中,
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; // get an internal reference let x = &data[0]; // OH NO! `push` causes the backing storage of `data` to be reallocated. // Dangling pointer! Use after free! Alas! // (this does not compile in Rust) data.push(4); println!("{}", x); }
简单的作用域分析不足以防止此错误,因为 data
实际上确实在我们需要的范围内存在。然而,当我们对其进行引用时,它被更改了。这就是为什么 Rust 要求任何引用都必须冻结被引用对象及其所有者。