Rust での Singleton パターンの適用方法について。

用語

Singleton パターンとは、GoF 本に掲載されるデザインパターンの一つ。クラス (構造体) のインスタンスをただ一つに制限する事で、一つの共有データにアプリのどこからでもアクセスできるようにする。

サンプル

状況に応じた手法がいくつかある。

サンプル 1

遅延初期化 (読取専用)

以下では、MyData のインスタンスの作成に必要となるメンバが、sample から公開されていない。そのため、MyData のインスタンスは、sample が作るただ一つに制限される。


use crate::sample::MyData;

fn main() {
    let singleton = MyData::get();
    assert_eq!(singleton.val(), 42);
}

mod sample {
    use std::sync::LazyLock;

    static SINGLETON: LazyLock<MyData> = LazyLock::new(||
        MyData::new(42)
    );

    pub struct MyData {
        val: i32
    }
    
    impl MyData {
        pub fn get() -> &'static MyData {
            LazyLock::force(&SINGLETON)
        }

        pub fn val(&self) -> i32 {
            self.val
        }

        fn new(val: i32) -> Self {
            Self { val }
        }
    }
}

サンプル 2

遅延初期化 (書込可能)

以下では、前の例とは異なり、構造体へのアクセスを Mutex で同期している。なお、同期を構造体単位でなくフィールド単位にしたり、Mutex の代わりに RwLock やアトミック型を使う方法も考えられる (コード量と並列性からどれも一長一短がある)。


use crate::sample::MyData;

fn main() {
    let mut singleton = MyData::get();
    singleton.set_val(43);
    assert_eq!(singleton.val(), 43);
}

mod sample {
    use std::sync::{LazyLock, Mutex, MutexGuard};

    static SINGLETON: LazyLock<Mutex<MyData>> = LazyLock::new(||
        Mutex::new(MyData::new(42))
    );

    pub struct MyData {
        val: i32,
    }

    impl MyData {
        pub fn get() -> MutexGuard<'static, MyData> {
            LazyLock::force(&SINGLETON).lock().unwrap()
        }

        pub fn val(&self) -> i32 {
            self.val
        }

        pub fn set_val(&mut self, value: i32) {
            self.val = value
        }

        fn new(val: i32) -> Self {
            Self { val }
        }
    }
}

サンプル 3

手動初期化

以下では、遅延初期化のサンプルで使われていた Lazy に代わって OnceCell を使っている。これにより、アクセス時に自動的に既定値で初期化するのではなく、事前に引数を与えて初期化するようにしている。


use crate::sample::MyData;

fn main() {
    MyData::init(42);
    assert_eq!(MyData::get().val(), 42);
}

mod sample {
    use std::sync::OnceLock;

    static SINGLETON: OnceLock<MyData> = OnceLock::new();

    #[derive(Debug)]
    pub struct MyData {
        val: i32,
    }

    impl MyData {
        pub fn init(val: i32) {
            SINGLETON
                .set(MyData::new(val))
                .expect("Should be called only one time.");
        }

        pub fn get() -> &'static MyData {
            SINGLETON
                .get()
                .expect("Should be called after initialization.")
        }

        pub fn val(&self) -> i32 {
            self.val
        }

        fn new(val: i32) -> Self {
            Self { val }
        }
    }
}