析构函数
在介绍堆时,我们提到你需要负责释放你分配的内存。在介绍借用检查器时,我们也说过,在Rust中你很少需要直接管理内存。
这两个陈述起初可能看似矛盾。让我们通过引入作用域和析构函数来看看它们是如何结合在一起的。
作用域
变量的作用域是指Rust代码中该变量有效或存活的区域。
变量的作用域从其声明开始。当以下之一发生时结束:
- 变量被声明的代码块(即
{}
之间)结束fn main() { // `x` is not yet in scope here let y = "Hello".to_string(); let x = "World".to_string(); // <-- x's scope starts here... let h = "!".to_string(); // | } // <-------------- ...and ends here
- 变量的所有权转移到其他人(例如,函数或其他变量)
fn compute(t: String) { // Do something [...] } fn main() { let s = "Hello".to_string(); // <-- s's scope starts here... // | compute(s); // <------------------- ..and ends here // because `s` is moved into `compute` }
析构函数
当一个值的所有者超出作用域时,Rust会调用其析构函数。析构函数试图清理该值使用的资源,尤其是它分配的内存。
你可以通过将值传递给std::mem::drop
来手动调用析构函数。这就是为什么Rust开发者常会说某个值已经被丢弃,以此来表达一个值已经超出作用域且其析构函数已被调用。
可视化解构点
我们可以通过插入显式的drop
调用来“拼写”出编译器为我们所做的操作。回到之前的例子:
fn main() { let y = "Hello".to_string(); let x = "World".to_string(); let h = "!".to_string(); }
这等同于:
fn main() { let y = "Hello".to_string(); let x = "World".to_string(); let h = "!".to_string(); // Variables are dropped in reverse order of declaration drop(h); drop(x); drop(y); }
来看第二个例子,其中s
的所有权被转移到compute
:
fn compute(s: String) { // Do something [...] } fn main() { let s = "Hello".to_string(); compute(s); }
它等同于:
fn compute(t: String) { // Do something [...] drop(t); // <-- Assuming `t` wasn't dropped or moved // before this point, the compiler will call // `drop` here, when it goes out of scope } fn main() { let s = "Hello".to_string(); compute(s); }
注意区别:尽管在main
中调用compute
后s
不再有效,但在main
中并没有s
的drop(s)
。
当你将值的所有权转移到函数时,你也正在转移清理它的责任。
这确保了一个值的析构函数至多被调用一次,设计上防止了双重释放漏洞。
丢弃后的使用
如果你尝试在值被丢弃后使用它会发生什么?
#![allow(unused)] fn main() { let x = "Hello".to_string(); drop(x); println!("{}", x); }
如果你尝试编译这段代码,你会收到错误:
#![allow(unused)] fn main() { error[E0382]: use of moved value: `x` --> src/main.rs:4:20 | 3 | drop(x); | - value moved here 4 | println!("{}", x); | ^ value used here after move }
Drop消耗掉它被调用的对象,意味着调用后该对象不再有效。因此,编译器会阻止你使用它,避免了释放后使用漏洞。
引用的丢弃
如果变量包含引用会怎样? 例如:
#![allow(unused)] fn main() { let x = 42i32; let y = &x; drop(y); }
当你调用drop(y)
...什么也没发生。如果你真的尝试编译这段代码,你会收到警告:
warning: calls to `std::mem::drop` with a reference
instead of an owned value does nothing
--> src/main.rs:4:5
|
4 | drop(y);
| ^^^^^-^
| |
| argument has type `&i32`
|
这回到了我们之前所说:我们只想调用一次析构函数。你可能对同一个值有多个引用——如果我们中的一个超出作用域时就调用它们指向的值的析构函数,其他引用会怎么样? 它们会指向一个不再有效的内存位置:一个所谓的悬挂指针,是释放后使用漏洞的近亲。Rust的所有权制度从设计上排除了这类漏洞。
参考资料
- 本节练习位于
exercises/03_ticket_v1/11_destructor
Rust不保证析构函数一定会执行。例如,如果你选择故意泄露内存,它们就不会执行。