析构函数

语言确实提供的是通过Drop trait 实现的完全自动化的析构函数,它提供了以下方法:

fn drop(&mut self);

这个方法给类型一些时间来完成它正在做的事情。

drop 运行后,Rust 将递归地尝试 drop self 的所有字段。

这是一个方便的功能,这样你就不必编写“析构函数样板代码”来 drop 子项。如果一个结构体除了 drop 其子项之外没有特殊的 drop 逻辑,那么这意味着根本不需要实现 Drop

在 Rust 1.0 中,没有稳定的方法来阻止这种行为。

请注意,获取 &mut self 意味着即使你可以阻止递归 Drop,Rust 也会阻止你,例如,将字段移出 self。对于大多数类型,这完全没问题。

例如,一个 Box 的自定义实现可能会像这样编写 Drop

#![feature(ptr_internals, allocator_api)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::mem;
use std::ptr::{drop_in_place, NonNull, Unique};

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>())
        }
    }
}
fn main() {}

这工作得很好,因为当 Rust 去 drop ptr 字段时,它只看到一个没有实际 Drop 实现的Unique。类似地,没有任何东西可以使用-后-释放 ptr,因为当 drop 退出时,它变得不可访问。

然而,这样做行不通:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, Global, GlobalAlloc, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Box<T> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // Hyper-optimized: deallocate the box's contents for it
            // without `drop`ing the contents
            let c: NonNull<T> = self.my_box.ptr.into();
            Global.deallocate(c.cast::<u8>(), Layout::new::<T>());
        }
    }
}
fn main() {}

在我们在 SuperBox 的析构函数中释放了 box 的 ptr 后,Rust 会很高兴地继续告诉 box 自己 drop,然后所有东西都会因为使用-后-释放和双重释放而崩溃。

请注意,递归 drop 行为适用于所有结构体和枚举,无论它们是否实现了 Drop。因此,类似这样的东西:

#![allow(unused)]
fn main() {
struct Boxy<T> {
    data1: Box<T>,
    data2: Box<T>,
    info: u32,
}
}

当它“应该”被 drop 时,将调用其 data1data2 字段的析构函数,即使它本身没有实现 Drop。我们说这样的类型需要 Drop,即使它本身不是 Drop。

类似地,

#![allow(unused)]
fn main() {
enum Link {
    Next(Box<Link>),
    None,
}
}

当且仅当实例存储了 Next 变体时,其内部的 Box 字段才会被 drop。

总的来说,这工作得非常好,因为你在重构数据布局时不必担心添加/删除 drop。然而,仍然有许多有效的使用场景需要对析构函数进行更棘手的处理。

覆盖递归 drop 并允许在 drop 期间移出 Self 的经典安全解决方案是使用 Option:

#![feature(allocator_api, ptr_internals)]

use std::alloc::{Allocator, GlobalAlloc, Global, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;

struct Box<T>{ ptr: Unique<T> }

impl<T> Drop for Box<T> {
    fn drop(&mut self) {
        unsafe {
            drop_in_place(self.ptr.as_ptr());
            let c: NonNull<T> = self.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
        }
    }
}

struct SuperBox<T> { my_box: Option<Box<T>> }

impl<T> Drop for SuperBox<T> {
    fn drop(&mut self) {
        unsafe {
            // Hyper-optimized: deallocate the box's contents for it
            // without `drop`ing the contents. Need to set the `box`
            // field as `None` to prevent Rust from trying to Drop it.
            let my_box = self.my_box.take().unwrap();
            let c: NonNull<T> = my_box.ptr.into();
            Global.deallocate(c.cast(), Layout::new::<T>());
            mem::forget(my_box);
        }
    }
}
fn main() {}

然而,这具有相当奇怪的语义:你是在说一个应该总是 Some 的字段可能是 None,仅仅是因为在析构函数中发生的事情。当然,反过来说,这很有道理:你可以在析构函数期间调用 self 上的任意方法,这应该阻止你在取消初始化字段后这样做。但这并不能阻止你在其中产生任何其他任意的无效状态。

总的来说,这是一个不错的选择。当然,这是你应该默认使用的。但是,在未来,我们希望有一种一流的方式来声明某个字段不应该被自动 drop。