确定性析构
说明
Rust不提供与finally
等价的代码块——也就是不管函数怎么结束都会执行的代码。相反,一个对象的析构器将会执行在退出前必须执行的代码。
代码示例
fn bar() -> Result<(), ()> {
// These don't need to be defined inside the function.
struct Foo;
// Implement a destructor for Foo.
impl Drop for Foo {
fn drop(&mut self) {
println!("exit");
}
}
// The dtor of _exit will run however the function `bar` is exited.
let _exit = Foo;
// Implicit return with `?` operator.
baz()?;
// Normal return.
Ok(())
}
出发点
如果一个函数有多个返回语句,那么在退出时执行析构代码将会是困难且重复的(并且容易产生bug)。使用宏来隐式地退出是一个例外。一个常见的用法是使用?
操作符,
当结果是Ok
的时候继续,当结果是Err
的时候返回。?
操作符是用来处理异常的一个机制,但是并不像Java的finally
,
这里不支持在正常情况和异常情况下都执行的代码。发生恐慌(Panicking)也将提前结束函数。
优点
在析构器里的代码退出前总是会被执行,能应对恐慌(panics),提前返回等等。
缺点
不保证析构器里的代码一定会被执行。举例来说,函数内有一个死循环或者在退出前函数崩溃的情况。在一个已经发生恐慌(panicking)的线程里再次发生恐慌时,析构器也不会执行代码。因此析构器也不能用于必须确定执行的情景。
这种模式介绍了一些难以注意的隐式代码,即函数在结束时没有显式给出析构器执行代码。因此导致debug的时候更加棘手。
为了确定性,申请一个对象和实现Drop
特性增加了很多样板代码。
讨论
下面是一些关于如何用对象做终结器(finaliser)的精妙之处。对象在函数结束前必须保持存活,然后就被销毁。
这个对象必须是一个值或者独占数据的指针(例如:Box<Foo>
)。如果使用一个共享指针(例如Rc
),
那么终结器的生命周期就比函数更长了。类似地,终结器不应该被转移所有权到他处或者被返回。
终结器必须绑定在变量上,否则当退出临时的作用域时它就会被销毁。如果变量仅用作终结器,变量的名字必须用_
开头,
否则编译器就会警告这个变量未使用。然而,不要直接用_
作为变量名称,这样的话将会立刻销毁这个变量。
在Rust中,析构器在对象离开作用域的时候执行。无论是到达代码块的末端、提前返回亦或是函数恐慌(panic)都属于这种情况。当恐慌发生时, Rust对每个栈帧中的每个对象执行析构器代码。所以析构器即使在函数调用内出现恐慌也能顺利执行。
如果一个析构器在析构时出现了恐慌,这就没啥好办法了,所以Rust不再执行析构,果断终止这个线程。这就意味着Rust并不是绝对保证析构器一定会执行,因此可能会导致资源泄露。
参阅
RAII.