析构器中的最终处理
Description
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(())
}
动机
如果一个函数有多个返回点,那么在退出时执行代码就会变得困难和重复(从而容易产生错误)。
特别是在由于宏而隐式返回的情况下。
一个常见的情况是?
操作符,如果结果是Err
就返回,如果是Ok
就继续。
?
被用作异常处理机制,但不像Java(有finally
),没有办法安排代码在正常和异常情况下运行。
发生Panic也会提前退出函数。
优势
析构器中的代码将(几乎)一直运行——应对panic、提前返回等问题。
劣势
事实上,并没有保证析构器一定会运行。 例如,如果在一个函数中存在一个无限循环,或者如果运行函数在退出前崩溃。 在已经发生panic的线程中,析构器也不会被运行。 因此,在绝对有必要进行最终处理的情况下,不能依靠析构器作为最终处理器。
这种模式引入了一些难以察觉的隐式代码。阅读一个函数时,没有明确指出退出时要运行哪些析构器。 这可能会使调试工作变得棘手。
要求对象和Drop
实现若只是为了最终处理,会是很沉重的模板代码。
讨论
关于如何准确地存储作为最终处理器的对象,有一些微妙的问题。
它必须被保存到函数结束,然后必须被销毁。
该对象必须始终是一个值或唯一拥有的指针(例如,Box<Foo>
)。
如果使用一个共享的指针(如Rc
),那么最终处理器可以在函数的生命周期之外保持生存。
出于类似的原因,最终处理器不应该被移动或返回。
最终处理器必须被分配到一个变量中,否则它将被立即销毁,而不是当它超出作用域时。
如果该变量只作为最终处理器使用,其名称必须以_
开头,否则编译器会警告说最终处理器从未被使用。
然而,不要调用没有后缀的变量_
——在这种情况下,它将被立即销毁。
在Rust中,当一个对象超出作用域时,会运行析构器。 这发生在我们到达块的末尾,有一个早期返回,或者程序发生panic。 当发生panic时,Rust会展开堆栈,为每个堆栈帧中的每个对象运行析构器。 因此,即使panic发生在被调用的函数中,析构器也会被调用。
如果在展开的过程中一个析构器发生panic,那么就没有好的行动可以采取,所以Rust会立即中止线程,而不再运行其他的析构器。 这意味着析构器不能绝对保证运行。 这也意味着你必须在你的析构器中格外小心,不要panic,因为它可能会让资源处于一个意想不到的状态。
参见
Latest commit 9834f57 on 25 Aug 2021