析构函数
语言确实提供的是通过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 时,将调用其 data1
和 data2
字段的析构函数,即使它本身没有实现 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。