析构器中的最终处理

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,因为它可能会让资源处于一个意想不到的状态。

参见

RAII守护对象

Latest commit 9834f57 on 25 Aug 2021