lazy_static
遅延初期化でメモリ効率が良いグローバル参照可能なリファレンス定義的な何か。
世の中的にはLacyCellに行くところなのでしょうけれども利用目的によってはlazy_staticのほうが適している場合も。
!!!情報修正!!!
ここはlazyLockに行くべきでした。
@namn1125(namnium) 様よりご指摘があり、完全にわたしが見定めを誤った部分です。同じことが出来るのであれば正式に採用されてるものを使う方が絶対いいので利用する場合はLazyLockにしましょう。ただほぼ内容は一緒なのでLazyLock版は最後に追記しました。
crate
標準機能として使える状態にはなっていないので cargo add lazy-static
でCargo.tomlへ追加しておく。
ここではCargo.toml
のdependenciesにはlazy_static = "1.5.0"
として取り込まれた。
基本
use lazy_static::lazy_static;
use std::sync::Mutex;
// グローバル参照したい構造体 いくつあっても良い。
#[derive(Default, Debug)]
struct AppData {
name: String,
}
#[derive(Default, Debug)]
struct AppData2 {
data: Vec<u8>,
}
// グローバル参照の為のリファレンスを定義
// 複数あるなら複数のリファレンスを作成する。
lazy_static! {
static ref APP_DATA: Mutex<AppData> = Mutex::new(AppData::default());
static ref APP_DATA2: Mutex<AppData2> = Mutex::new(AppData2::default());
}
// 構造体から名前を取得して返す
fn get_name() -> String {
let appdata = APP_DATA.lock().unwrap();
appdata.name.clone()
}
// 構造体の名前をセットする
fn set_name(name: String) {
let mut appdata = APP_DATA.lock().unwrap();
appdata.name = name;
}
~ 略 ~
これでこのコードが書いてあるソースが見えている範囲からget_name()
とset_name()
を呼び出して値の参照や更新が可能になる。
グローバル変数みたいなものなのでイメージは良くないんですが、使ってみると超便利。
具体的にどう便利なのか
Tauriやその他フレームワーク的な何かそういった仕組みに乗っかる場合に値を保持したい為にいちいちDBを利用したり永続化したりしなければならないことがありオーバーヘッドが気になる。
構造体を初期化して持ちまわれたらいいのに…。イベントドリブンだからどうやって持ち回ればいいかわからないよ。など。
そんなときに便利。
LazyCellはミュータブルを扱えないので値を変更したい場合はlazy_staticの出番になります。
LazyCellはミュータブルが扱えないのではなくスレッドがダメなのだそうです。
遅延初期化によるメモリの効率の良さについては…ごめん、よくわからない。(理解としては巨大なリストとかを持たないといけない場合に起動時にそれを処理せず利用するときにはじめて初期化等するのでソフトの起動が速くなる)大きなプロジェクトなら恩恵があるかもしれません~。
疑問点
get_name()
やset_name()
の、セッターとゲッターみたいなのを一通り定義しなきゃいけないとかあまり考えたくなかったのでこんな風に書いてみた。
// lock().unwrap()をいちいち書きたくなかった。
fn app_data() -> std::sync::MutexGuard<'static, AppData> {
APP_DATA.lock().unwrap()
}
// セッターゲッターを作らずにアクセスしたほうがスマートじゃない?
let mut appdata = app_data();
appdata.name = "Hanako".to_string();
しかし、いざこのようにして動作させてみたところプログラムが動かない。
APP_DATA.lock().unwrap()
の手間を省いたのが原因かと思い、app_data()
関数を使わずに真面目にかいてみたら(しかもその方が簡単だった)それでもだめ。
結局セッターゲッター的な関数を定義してそれを使わないとうまくいかない。なぜ…。
原因と対策
原因はlock()
が解除されずに次のlock()
が呼ばれてしまうことによるデッドロック。
ロックが解放されるようにすればよく、つまりスコープに入れてやればいい。
関数になっていると問題が起きないのはそういうことだった。
{
let mut appdata = APP_DATA.lock.unwrap();
appdata.name = "Takao".to_string();
}
このようにスコープに入れてやることで動作した。
つまり値を返してもらう場合はクロージャで処理すれば良い。
let name = || -> String {
let appdata = APP_DATA.lock().unwrap();
appdata.name.clone()
}();
分かってしまえば何ということはないのだけれどこれで半日吹き飛ぶ程度にはハマったので備忘録的に残しておきたい。
使い方 改め
まず最低限必要なリファレンス定義まで。
use lazy_sstatic::lazy_static;
use std::sync::Mutex;
// グローバル参照したい構造体
#[derive(Default, Debug)]
struct AppData {
name: String,
}
// グローバル参照の為のリファレンスを定義
lazy_static! {
static ref APP_DATA: Mutex<AppData> = Mutex::new(AppData::default());
}
実際の値の参照や更新は次のようにする。
前述の通り、スコープに入れてやることが重要なので注意。
// 値のセット
{
let mut appdata = APP_DATA.lock().unwrap();
appdata.name = "Takao".to_string();
}
// 値の受け取り
let name = || -> String {
let appdata = APP_DATA.lock().unwrap();
appdata.name.clone()
}();
LazyLock版
前述のとおり、見定めを誤った(情報をうまくつかむことが出来ていなかった)のでLazyCellやLazyLockはOnceCell的なことしかできないのだと勘違いをしました。
lazy_Staticと同じことがLazyLockで可能です。
LazyLockは正式に採用された機能ですので利用するときはこちらを使う様にしましょう。
ということで以下が利用方法のコードです。
微妙に書き方が違う事、crateの導入が必要ないことなどの違いはあるけれどここまで読んでいただけたなら内容は解ると思います。
また、クロージャで書いていた部分も普通のブロックに書き換えました。クロージャにする意味あまりないし、普通がいいよね
use std::sync::{Mutex, LazyLock};
#[derive(Default, Debug)]
struct AppData {
name: String,
}
//参照の定義
static APP_DATA: LazyLock<Mutex<AppData>> = LazyLock::new(|| Mutex::new(AppData::default()));
fn global_test(){
{
let mut appdata = APP_DATA.lock().unwrap();
appdata.name = "Hanako".to_string();
};
let name = {
let appdata = APP_DATA.lock().unwrap();
appdata.name.clone()
};
println!("{}", name);
}
fn main() {
global_test();
}