强制删除结构字段的顺序
Forcing the order in which struct fields are dropped
我正在实现一个对象,该对象拥有多个通过 FFI 从 C 库创建的资源。为了在构造函数崩溃时清理已经完成的工作,我将每个资源包装在它自己的结构中并为它们实现 Drop
。但是,当谈到删除对象本身时,我不能保证资源将以安全的顺序删除,因为 Rust 没有定义结构字段的删除顺序。
通常,您可以通过使对象不拥有资源而是借用资源(以便资源可以相互借用)来解决此问题。实际上,这将问题推向了调用代码,其中放置顺序被明确定义并通过借用语义强制执行。但这不适合我的用例,而且通常有点逃避。
令人愤怒的是,如果 drop
出于某种原因采用 self
而不是 &mut self
,这将非常容易。然后我可以按我想要的顺序调用 std::mem::drop
。
有什么办法吗?如果没有,有没有办法在构造函数恐慌的情况下清理而无需手动捕获和重新恐慌?
您可以通过两种方式指定结构字段的放置顺序:
隐式
我写了 RFC 1857 指定下降顺序,它已于 2017/07/03 合并!根据 RFC,结构字段的删除顺序与声明的顺序相同。
您可以通过运行下面的示例
进行检查
struct PrintDrop(&'static str);
impl Drop for PrintDrop {
fn drop(&mut self) {
println!("Dropping {}", self.0)
}
}
struct Foo {
x: PrintDrop,
y: PrintDrop,
z: PrintDrop,
}
fn main() {
let foo = Foo {
x: PrintDrop("x"),
y: PrintDrop("y"),
z: PrintDrop("z"),
};
}
输出应该是:
Dropping x
Dropping y
Dropping z
明确
RFC 1860 introduces the ManuallyDrop
类型,包装另一种类型并禁用其析构函数。这个想法是你可以通过调用一个特殊的函数(ManuallyDrop::drop
)手动删除对象。此函数不安全,因为删除对象后内存未初始化。
您可以使用 ManuallyDrop
在您的类型的析构函数中明确指定字段的放置顺序:
#![feature(manually_drop)]
use std::mem::ManuallyDrop;
struct Foo {
x: ManuallyDrop<String>,
y: ManuallyDrop<String>
}
impl Drop for Foo {
fn drop(&mut self) {
// Drop in reverse order!
unsafe {
ManuallyDrop::drop(&mut self.y);
ManuallyDrop::drop(&mut self.x);
}
}
}
fn main() {
Foo {
x: ManuallyDrop::new("x".into()),
y: ManuallyDrop::new("y".into())
};
}
如果您需要这种行为而不能使用任何一种较新的方法,请继续阅读...
掉落问题
drop
方法不能按值获取其参数,因为该参数会在作用域末尾再次被丢弃。这将导致语言的所有析构函数无限递归。
一个可能的solution/workaround
我在一些代码库中看到的一种模式是将要删除的值包装在 Option<T>
中。然后,在析构函数中,您可以将每个选项替换为 None
并以正确的顺序删除结果值。
例如,在 scoped-threadpool 包中,Pool
对象包含线程和一个将安排新工作的发送者。为了在删除时正确加入线程,应该先删除发送者,然后再删除线程。
pub struct Pool {
threads: Vec<ThreadData>,
job_sender: Option<Sender<Message>>
}
impl Drop for Pool {
fn drop(&mut self) {
// By setting job_sender to `None`, the job_sender is dropped first.
self.job_sender = None;
}
}
关于人体工程学的注释
当然,以这种方式做事与其说是正确的解决方案,不如说是一种变通方法。此外,如果优化器无法证明该选项将始终为 Some
,您现在有一个额外的分支用于每次访问您的结构字段。
幸运的是,没有什么能阻止 Rust 的未来版本实现允许指定放置顺序的功能。它可能需要一个 RFC,但似乎确实可行。关于为该语言指定删除顺序的问题跟踪器正在进行 discussion,尽管它在上个月一直处于非活动状态。
安全注意事项
如果以错误的顺序破坏你的结构是不安全的,你应该考虑制作它们的构造函数 unsafe
并记录这个事实(以防你还没有这样做)。否则,仅通过创建结构并让它们超出范围就可能触发不安全行为。
我正在实现一个对象,该对象拥有多个通过 FFI 从 C 库创建的资源。为了在构造函数崩溃时清理已经完成的工作,我将每个资源包装在它自己的结构中并为它们实现 Drop
。但是,当谈到删除对象本身时,我不能保证资源将以安全的顺序删除,因为 Rust 没有定义结构字段的删除顺序。
通常,您可以通过使对象不拥有资源而是借用资源(以便资源可以相互借用)来解决此问题。实际上,这将问题推向了调用代码,其中放置顺序被明确定义并通过借用语义强制执行。但这不适合我的用例,而且通常有点逃避。
令人愤怒的是,如果 drop
出于某种原因采用 self
而不是 &mut self
,这将非常容易。然后我可以按我想要的顺序调用 std::mem::drop
。
有什么办法吗?如果没有,有没有办法在构造函数恐慌的情况下清理而无需手动捕获和重新恐慌?
您可以通过两种方式指定结构字段的放置顺序:
隐式
我写了 RFC 1857 指定下降顺序,它已于 2017/07/03 合并!根据 RFC,结构字段的删除顺序与声明的顺序相同。
您可以通过运行下面的示例
进行检查struct PrintDrop(&'static str);
impl Drop for PrintDrop {
fn drop(&mut self) {
println!("Dropping {}", self.0)
}
}
struct Foo {
x: PrintDrop,
y: PrintDrop,
z: PrintDrop,
}
fn main() {
let foo = Foo {
x: PrintDrop("x"),
y: PrintDrop("y"),
z: PrintDrop("z"),
};
}
输出应该是:
Dropping x
Dropping y
Dropping z
明确
RFC 1860 introduces the ManuallyDrop
类型,包装另一种类型并禁用其析构函数。这个想法是你可以通过调用一个特殊的函数(ManuallyDrop::drop
)手动删除对象。此函数不安全,因为删除对象后内存未初始化。
您可以使用 ManuallyDrop
在您的类型的析构函数中明确指定字段的放置顺序:
#![feature(manually_drop)]
use std::mem::ManuallyDrop;
struct Foo {
x: ManuallyDrop<String>,
y: ManuallyDrop<String>
}
impl Drop for Foo {
fn drop(&mut self) {
// Drop in reverse order!
unsafe {
ManuallyDrop::drop(&mut self.y);
ManuallyDrop::drop(&mut self.x);
}
}
}
fn main() {
Foo {
x: ManuallyDrop::new("x".into()),
y: ManuallyDrop::new("y".into())
};
}
如果您需要这种行为而不能使用任何一种较新的方法,请继续阅读...
掉落问题
drop
方法不能按值获取其参数,因为该参数会在作用域末尾再次被丢弃。这将导致语言的所有析构函数无限递归。
一个可能的solution/workaround
我在一些代码库中看到的一种模式是将要删除的值包装在 Option<T>
中。然后,在析构函数中,您可以将每个选项替换为 None
并以正确的顺序删除结果值。
例如,在 scoped-threadpool 包中,Pool
对象包含线程和一个将安排新工作的发送者。为了在删除时正确加入线程,应该先删除发送者,然后再删除线程。
pub struct Pool {
threads: Vec<ThreadData>,
job_sender: Option<Sender<Message>>
}
impl Drop for Pool {
fn drop(&mut self) {
// By setting job_sender to `None`, the job_sender is dropped first.
self.job_sender = None;
}
}
关于人体工程学的注释
当然,以这种方式做事与其说是正确的解决方案,不如说是一种变通方法。此外,如果优化器无法证明该选项将始终为 Some
,您现在有一个额外的分支用于每次访问您的结构字段。
幸运的是,没有什么能阻止 Rust 的未来版本实现允许指定放置顺序的功能。它可能需要一个 RFC,但似乎确实可行。关于为该语言指定删除顺序的问题跟踪器正在进行 discussion,尽管它在上个月一直处于非活动状态。
安全注意事项
如果以错误的顺序破坏你的结构是不安全的,你应该考虑制作它们的构造函数 unsafe
并记录这个事实(以防你还没有这样做)。否则,仅通过创建结构并让它们超出范围就可能触发不安全行为。