如何使Rust单例的析构函数运行?

3

以下是我所知道的在Rust中创建单例的方法:

#[macro_use]
extern crate lazy_static;

use std::sync::{Mutex, Once, ONCE_INIT};

#[derive(Debug)]
struct A(usize);
impl Drop for A {
    fn drop(&mut self) {
        // This is never executed automatically.
        println!(
            "Dropping {:?} - Important stuff such as release file-handles etc.",
            *self
        );
    }
}

// ------------------ METHOD 0 -------------------
static PLAIN_OBJ: A = A(0);

// ------------------ METHOD 1 -------------------
lazy_static! {
    static ref OBJ: Mutex<A> = Mutex::new(A(1));
}

// ------------------ METHOD 2 -------------------
fn get() -> &'static Mutex<A> {
    static mut OBJ: *const Mutex<A> = 0 as *const Mutex<A>;
    static ONCE: Once = ONCE_INIT;
    ONCE.call_once(|| unsafe {
        OBJ = Box::into_raw(Box::new(Mutex::new(A(2))));
    });
    unsafe { &*OBJ }
}

fn main() {
    println!("Obj = {:?}", PLAIN_OBJ); // A(0)
    println!("Obj = {:?}", *OBJ.lock().unwrap()); // A(1)
    println!("Obj = {:?}", *get().lock().unwrap()); // A(2)
}

在程序退出时,这些调用均未调用A的析构函数(drop())。 Method 2(堆分配)的预期行为是如此,但我没有研究过lazy_static!的实现,因此不知道它是否会类似。

这里没有RAII。 我可以使用函数本地静态变量在C ++中实现RAII单例(我以前一直在编写C ++代码,因此我的大多数比较都与C ++相关 - 我不了解其他许多语言):

A& get() {
  static A obj; // thread-safe creation with C++11 guarantees
  return obj;
}

这可能是在实现定义的区域中分配/创建的(懒惰地),并且在程序的生命周期内有效。当程序终止时,析构函数会被确定性地运行。我们需要避免从其他静态变量的析构函数中访问它,但我从未遇到过这种情况。
我可能需要释放资源,并希望运行drop()。目前,我最终手动执行此操作(在所有线程加入等之后,在main函数结尾处)。
我甚至不知道如何使用lazy_static!来做到这一点,因此我避免使用它,只选择方法2,在最后可以手动销毁它。
我不想这样做;在Rust中有没有一种方法可以拥有这样的RAII单例?
1个回答

8
特别是单例模式以及全局构造函数/析构函数通常是一种祸根(特别是在像C++这样的语言中)。
我认为它们带来的主要(功能性)问题分别被称为静态初始化(resp. destruction)顺序灾难。也就是说,很容易意外地在这些全局变量之间创建依赖循环,并且即使没有这样的循环,编译器也不会立即清楚应该按照什么顺序构建/销毁它们。
它们还可能引起其他问题:启动速度较慢,意外共享内存等等。
在Rust中,采取的态度是“main之前/之后没有生命”。因此,尝试获得C ++行为可能不会按预期工作。
如果您:
放弃全局方面 放弃尝试拥有单个实例
您将获得更大的语言支持(并且作为奖励,它也更容易并行测试)。
因此,我的建议是简单地坚持使用本地变量。在main中实例化它,通过值/引用向下调用堆栈传递它,不仅避免了那些棘手的初始化顺序问题,还可以获得销毁。

4
我们需要一个机器人,能够自动回复每个带有“singleton”一词的问题,并回复“你真的不想这样做”。 - Shepmaster
除了这里提到的优秀观点,我还要将多线程添加到问题案例列表中。 - Shepmaster
2
它有它的用处——这里有一个在Rust中的简单示例:假设你需要一个单一对象在所有测试中持久存在(这些测试在cargo test期间作为独立线程并行运行)。也许它正在模拟网络,在一堆基于全局参数的转换之后写入单个文件,这些参数随着测试运行而改变,并且对其他测试看到更改很重要。这种模式太抽象了,不能轻易地被丢弃,即使有些应该很少使用,它们仍然有它们的用处。但是你回答了我的问题,即它不可能——所以如果我没有其他选择,我会接受这个答案——谢谢。 - ustulation
1
@ustulation:也许这个可能会有用,毕竟lazy_static!宏是存在的。然而请注意,lazy_static!通过延迟创建来避免了初始化顺序问题,并且通过永不销毁来避免了销毁顺序问题。在测试的情况下...我不太愿意在随机调度的并行测试之间共享一个公共项,这听起来像是导致虚假失败的风险。 - Matthieu M.
@ustulation 我同意它确实有用的情况,但我认为对于每一种好的使用方法,都存在大量的误用。我认为你提到的“测试中的虚假网络”是一个误用的例子,正如Matthieu M.所支持的那样。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接