确定性析构

说明

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.