🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

4回答

13189閲覧

Rustでグローバル変数に設定データを置きたい

rust

総合スコア5

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

3グッド

4クリップ

投稿2021/01/27 13:57

編集2021/01/27 14:04

Rustでグローバル変数に設定データを置きたい

Rustの勉強を少し前から始めました。

Rustのプログラムで全体から参照される設定データを持つ方法を探しています。今回はサンプルですので、コマンドライン引数の0番目と1番目、つまり自分自身の名前と最初の引数を保持することにします。実用面はないかも知れませんが、サンプルということでご了承ください。

練習帳に次のようなコードを書きました。一応コンパイルは通りますが、釈然としない点がいくつかあります。

use lazy_static::lazy_static; lazy_static! { static ref argv0: String = { let args: Vec<String> = std::env::args().collect(); let s = args[0].clone(); s }; static ref argv1: String = String::new(); } fn get_str_ref() -> &'static str { &argv0 } pub(crate) fn init(args: Vec<String>) { println!("{}", get_str_ref()); // println!("{}", &argv0); }

1. init()でグローバル変数を初期化したい

色々調べたところ、Rustではグローバル変数は推奨されていないようです。確かに、野放図にグローバル変数を使うと、どこからアクセスされるかわからないし、バグの温床なのはわかります。

しかし、アプリの起動時など(設定ファイルをロードしたときとか)に値を設定して、あとは基本的に変更しない(読み込みだけ)場合はグローバル変数は便利だと思います。

今回はVec<String>でコマンドライン引数を受け取ることにします。コンパイルを通すためにlazy_static!の中でstd::env::args().collect()していますが、これをしたくありません。これは例であって、呼び出し元から情報を渡したり、あるいは何らかの処理をした結果をグローバル変数に入れたいと思います。例えばアプリ名のMD5ハッシュを記録しておきたいなど。

lazy_static!内で初期化したくない、init()で初期化したい

わかっていないだけかも知れませんが、lazy_static!には外部から情報を渡して初期化できないようですが、それだと使いにくいのです。外部で色々処理をして、その後にinit()を呼んでグローバル変数を設定したほうが自由度が高く自然だと思うのですが、これはRustの理解が足りないからでしょうか。

ミュータブルの方がいい場合もある

今回は初期化後はイミュータブル(固定)でよいのですが、今後はミュータブル(値書き換え可能)なグローバル変数を作りたくなるかも知れませんので、この場を借りて同時に質問させてください。

lazy_static!にStringを組み合わせた例が少ない

色々検索したのですが、どういうわけかlazy_static!にStringを用いた例があまり見つかりませんでした。

2. lazy_static!の値を参照したい

今回は単純にprintln!()していますが、値を参照する際によくわからない問題に遭遇しました。

fn get_str_ref() -> &'static str { &argv0 }

という関数を用意した上で

pub(crate) fn init(args: Vec<String>) { println!("{}", get_str_ref()); // println!("{}", &argv0); }

すると値を表示できます。しかしコメントアウトしてあるほうは

argv0` cannot be formatted with the default formatter

とか

`argv0` doesn't implement `std::fmt::Display`

というエラーが出ます。エラーの意味はおおよそ見当が付きます(C++のoperator<<みたいなものでたぶん独自の型でstd::fmt::Displayを実装しておけばprintln!()できるのでしょう)が、get_str_ref()はただリファレンスを取得して返すだけの関数です。そしてargv0はStringなのでprintln!()できるはずです。

この2つは何が違うのでしょうか?ただ値を表示するだけなのにリファレンスを取得する関数を書かないといけないのは不便なので原因を知りたいと思います。このリファレンス取得関数は英語の質問サイト(アドレス失念)で見つけました。

3. C++との違い

C++の問題点やRustの理念は一応本で読みました。確かにC++は野放図に使うとバグだらけで手に負えないプログラムになるので、Rustのように制約を課して危ないコードはコンパイルできないようにするのは理に適っていると思います。

しかし、グローバルの設定データのようなものは、適切にnamespaceを設定して、その中で変数名もxとかiみたいな外でも出てきそうな安易な名前を避ければ安全かつ簡単に扱えると思います。C++例:

namespace globalSettings { std::string programName; std::string workingDirectory; std::string userName; }

これならこの値を参照したい、あるいは書き換えたいときにglobalSettings::workingDirectoryを操作するだけです。間違って上書きすることも人並みの注意力があればしないでしょう。

Rustの所有権や借用の理念はわかりますが、このlazy_static!については不便すぎではないかと感じていますが、たぶん私がまだRustを始めたばかりで理解が浅いのだと思います。

漠然とした質問になりますが、こういう設定データを扱う場合のRustらしい書き方はありますか?lazy_static!を使ったこと自体がC++の考え方をひきずっているのかも知れないと思い、この質問を追加しました。

よろしくおねがいします。

hoshi-takanori, yohhoy, yui513👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tatsuya6502

2021/01/27 16:51 編集

たくさんの質問が書かれていますが、これですと回答を書く人の負担が大きそうです。(回答を書くのにまとまった時間が必要になる)  また、あとで質問と回答を読む人にとっても、誰の回答がどの質問に対するものなのかなどが分かりにくくなるかもしれません。 いくつかの小さな質問に分けてもらえないでしょうか? たとえば、以下のように3つくらいの質問に分けられると思います。 1. Rustでグローバル変数に設定データを置きたい(この質問) 1-1. init()でグローバル変数を初期化したい 1-2. グローバルなミュータブル変数を作りたい 2. lazy_static!の値を参照したい(別の質問として作成する) 3. 設定データを扱う場合のRustらしい書き方(別の質問として作成する)
tatsuya6502

2021/01/28 00:52 編集

2と3について、すでに回答が付いているようですので、質問は分けないでこのままにしておくのが良さそうですね。次回、また別の質問をされるときは質問範囲を狭くしていただけると助かります。
rust

2021/01/28 01:33

ありがとうございます。次回はそうします。
guest

回答4

0

二方から回答がついていますが、別の角度からの回答も加えてみようと思います。

アプリケーションの設定をプログラム起動時に初期化して、以後ずっと使い続けるというのはよくあるシチュエーションです。そういうときの典型的な書き方はデータを構造体に持たせて、データを使う処理はその構造体のメソッドにするというやり方です。

rust

1struct App { 2 s: String, 3} 4 5impl App { 6 pub fn init(mut args: Vec<String>) -> Self { 7 let app = Self { s: args.remove(0) }; 8 println!("{}", app.get_str_ref()); 9 app 10 } 11 12 pub fn get_str_ref(&self) -> &str { 13 &self.s 14 } 15}

ミュータブルにしたければ &mut self とすれば変更できますし、安全でない使い方をしようとしたらRustのコンパイラがみつけてくれます。

投稿2021/01/27 23:04

blackenedgold

総合スコア468

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rust

2021/01/28 01:45

お返事が順不同になり申し訳ありません。理解できそうな順から考えています。 構造体に設定をもたせるのは考えて実装してみたことがあります。コンパイルは通り`lazy_static!`よりは自然だと感じましたが、今度はその構造体をどこに保持しておくかの問題が発生しませんか?この構造体自体をグローバル空間に置いておくという意味で合っていますか?
blackenedgold

2021/01/28 05:50

> この構造体自体をグローバル空間に置いておくという意味で合っていますか? いいえ。関数の引数で渡していく、あるいはメソッドを呼び出すときに暗黙に参照するという意味です。 > 今度はその構造体をどこに保持しておくかの問題が発生しませんか? 関数の引数で渡していくことになるかと思います。強いて「置き場」がどこかというなら `main` 関数内のローカル変数です。
rust

2021/02/01 05:06

ありがとうございます、意図を理解しました。以前別言語でそのような設計をしたこともあるのですが、深い階層に至るまで変数を渡していくと関数の引数が増えてしまったので、以後やらなくなりました。
blackenedgold

2021/02/05 05:04

それはよく知られている問題ですね。ガマンして渡し続けるか、依存性の注入(Dependency Injection、DI)というテクニックなどを使って解決することが多いと思います(他にも色々方法はあると思います)。 この辺りはソフトウェアの設計の話になって元の質問からは逸れるので興味があったら調べてみて下さい。
guest

0

ベストアンサー

最初にお伝えしておきたいのですが、Rustのグローバル変数(static)は根本的に不便なものです。
C++のようなグローバル変数はどのスレッドからでも好き勝手に触れるので、Rustのstaticは安全性を守るために以下の2つの方法でしか使えないようになっています。

  • (immutableな) static : コンパイル時に決定する初期値を使って、実行開始時に初期化してそれ以降変更しない
  • static mut : どこからでも好きに変更できるが、使用は自己責任(unsafe)

この不便さを緩和するために使われているのがlazy_staticなどのクレートです。

ところで、最近はlazy_staticではなくonce_cellなどの他のクレートを使うのがトレンドです。
と言うのも、lazy_staticは実装がマクロで隠蔽されているためわかりづらかったり、単純に利用する上で柔軟性が低かったりするためです。

以下の回答では once_cell を使ったコードを提示します。

  1. init()でグローバル変数を初期化したい

OnceCellを使うことで、static変数に後から値を入れることができます。

use once_cell::sync::OnceCell; static argv0: OnceCell<String> = OnceCell::new(); static argv1: OnceCell<String> = OnceCell::new(); pub(crate) fn init(args: Vec<String>) { let args: Vec<String> = std::env::args().collect(); argv0.set(args[0].clone()).unwrap(); argv1.set(String::new()).unwrap(); println!("{}", argv0.get().unwrap()); }
  1. lazy_static!の値を参照したい

lazy_staticは各変数に対してDerefを実装したユニークな型を実装しているようです。
つまり、argv0は型argv0の変数であって型Stringではないわけです。

Derefで元の型に戻るように実装されているので、&argv0Stringです。

(こういう隠れた型があることもlazy_staticが置き換えられた理由かもしれない)

  1. C++との違い

上の説明と重なりますが、C++のグローバル変数はRust的には非常に危険です。
Rustの設計思想としてコンパイル時にデータレースを起こすものを弾くようになっているので、そのためにstaticはこのような設計になっています。
もちろんその設計を超えて何かをしたいこともあるので、人間が全ての責任を負うstatic mutも用意されています。

回答が長くなってしまいすみません。

投稿2021/01/27 16:32

equal-l2

総合スコア172

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rust

2021/02/01 04:52

丁寧な回答ありがとうございます。お返事が遅くなって申し訳ありません。 `once_cell`は実に理想的に動きました。コードが簡潔で、かつやりたいことが実現できました。unwrap()について調べていたり横道にそれて検証が遅くなりました。 `argv0`は型`argv0`の変数というのには気づきませんでしたが、よく考えるとエラーメッセージがそのようなことを言っていたと思います。不注意でしたが、そのような可能性を考えておりませんでした。これもマクロによって隠蔽されている部分で型が作られているのでしょうか。 今回は片鱗でしたが「Rustの設計思想としてコンパイル時にデータレースを起こすものを弾くようになっている」ということを強く体験できました。
rust

2021/02/02 02:39

一通りご回答を読んで自分なりに解釈してみました。どの回答も示唆に富んでいて大変助かりました。悩んだのですが、回答順および `once_cell` について紹介してくださったことで、この回答をベストアンサーとさせていただきます。ありがとうございました。
guest

0

  1. lazy_static!の値を参照したい

この質問に対する回答です。

エラーになる理由は、すでに他の回答に書かれているとおりです。

lazy_staticは各変数に対してDerefを実装したユニークな型を実装しているようです。
つまり、argv0は型argv0の変数であって型Stringではないわけです。

Derefトレイトは参照外し(dereferencing)のタイミングで値を別の型へ変換するしくみを提供します。(ドキュメント) つまり、グローバル変数argv0の値は、*argv0のように参照外しすることでString型の値に変換できます。

従って、このエラーになる文を、

rust

1println!("{}", &argv0);

以下のように変更するとコンパイルエラーが解消します。

rust

1println!("{}", &*argv0);

なお、cargo-expandというコマンドを使うと、lazy_static!などのマクロが展開された結果を簡単に確認できます。

インストール

cargo-expandは以下のコマンドでインストールできます。なお、cargo-expandを実行時にRust nightlyツールチェーンが必要になりますので、rustupを使ってそちらもインストールします。

console

1$ cargo install cargo-expand 2$ rustup install nightly

実行

console

1$ cd <Cargo.tomlが置かれたディレクトリ> 2$ cargo expand

実行結果の例

rust

1struct argv0 { 2 __private_field: (), 3} 4#[doc(hidden)] 5static argv0: argv0 = argv0 { 6 __private_field: (), 7}; 8impl ::lazy_static::__Deref for argv0 { 9 type Target = String; 10 fn deref(&self) -> &String {

展開されたコードでは、lazy_static::__Derefというトレイトを実装していますが、これはDerefトレイトの別名です。

lazy-static.rs/src/lib.rs#L118

rust

1pub use core::ops::Deref as __Deref;

投稿2021/01/28 01:26

編集2021/02/01 09:39
tatsuya6502

総合スコア2046

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rust

2021/02/01 05:08

回答ありがとうございます。 `argv0`型の変数について直感的に理解が難しかったので追加の説明をいただき大変参考になりました。また `cargo-expand` についても教えてくださってありがとうございます。いまのところ動かすことに成功していないのですが、問題点を解決して使ってみたいと思います。
tatsuya6502

2021/02/01 09:36

cargo-expandが動かない件ですが、もし以下のようなエラーが出ているなら、rustupでRustのnightlyツールチェーンをインストールすることで解決します。 "error: the option `Z` is only accepted on the nightly compiler" 回答で説明が漏れていてすみません。回答の方にnightlyツールチェーンをインストールするよう追記しました。
rust

2021/02/02 02:36

ありがとうございます。 `rustup install nightly` して ` rustup run nightly cargo expand` で動きました。余談ですがCLionの白背景だと文字が白で読むのが困難だったのでTerminalの黒背景で実行しました。
tatsuya6502

2021/02/02 02:46

動いてよかったです! 細かい話ですが、cargo-expandのREADMEによると、 rustup run nightly ... はしなくて大丈夫かもしれません。 https://github.com/dtolnay/cargo-expand#installation > it requires a nightly toolchain to be installed, though does not require nightly ... the one with which cargo expand itself is executed. また、文字色についてですが、~/.cargo/config でthemeを設定することで変更できるようです。 https://github.com/dtolnay/cargo-expand#configuration
guest

0

ミュータブルの方がいい場合もある

今回は初期化後はイミュータブル(固定)でよいのですが、今後はミュータブル(値書き換え可能)なグローバル変数を作りたくなるかも知れませんので、この場を借りて同時に質問させてください。

この質問に対する回答です。

ミュータブルなグローバル変数ですが、 static mut 変数を使うのは未定義動作に遭遇することがあるためお勧めしません。詳しくはこちらの記事を参照してください。

ここではお勧めの方法のひとつとして、以下を組み合わせる方法を紹介します。

  • static 変数
  • once_cell クレート
  • std クレート(標準ライブラリ)の Mutex

static 変数は複数のスレッドがら同時にアクセスされる可能性があるので、ミュータブルにするには Mutex のような型でラップする必要があります。(そうしないとコンパイルできません) Mutex はマルチスレッド対応のロックの一種で、 Sync トレイトを実装した上で、内部のミュータビリティ(interior mutability)を提供します。

rust

1// Cargo.toml 2// 3// [dependencies] 4// once_cell = "1" 5 6use once_cell::sync::OnceCell; 7use std::{error::Error, sync::Mutex}; 8 9// 設定情報。設定自体はString型だが、ミュータブルかつマルチスレッドからの 10// アクセスに対応するために、Mutex型でラップする 11static MY_SETTING: OnceCell<Mutex<String>> = OnceCell::new(); 12 13fn main() -> Result<(), Box<dyn Error>> { 14 15 // 設定情報を初期化する 16 init()?; 17 println!("{}", MY_SETTING.get().unwrap().lock()?); 18 19 // 設定情報を変更する 20 MY_SETTING 21 .get() 22 .unwrap() 23 // ロックを取得する 24 .lock() 25 // ロックで守られた情報(String)を変更する 26 .map(|mut s| *s = "Updated setting".into())?; 27 28 println!("{}", MY_SETTING.get().unwrap().lock()?); 29 Ok(()) 30} 31 32fn init() -> Result<(), Box<dyn Error>> { 33 MY_SETTING 34 .set(Mutex::new("Initial setting".into())) 35 .map_err(|_| "Couldn't set MY_SETTING".into()) 36}

実行結果

console

1Initial setting 2Updated setting

ミュータブルなグローバル変数の使い方の詳細については、以下の記事を参照してください。

かなり古い記事なのでlazy_staticクレートを使っていますが、いまだとonce_cellの方が使いやすいかもしれません。

投稿2021/01/27 17:45

tatsuya6502

総合スコア2046

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rust

2021/02/01 05:04

お返事が遅くなって申し訳ありません。 結局、`once_cell` で対応しました。 というだけだと短すぎるので少し脱線します。以前にSwiftという言語をやったことがあります。Swiftはオプショナル型というRustにあるOption型と同じようなものがあります。Swiftではオプショナル型は `! ` で強制的に中身を取り出すことができますが、これが `nil` の場合はクラッシュします。ですので、ベストかはわかりませんが、教訓としては `!` を使わないことにしました(エディタで検索して `!` を使っていないことを確認しています)。 Rustに `unsafe` があることは聞いています。おそらくこれも言語が提供している安全弁を無効にするものなので、できる限り使わないほうがいいのでしょう。`static mut` に `unsafe` を毎回使ってアクセスするコードはそのうちバグを仕込みそうな予感がします。 今回のケースは初期化時など極めて限定されたケースでしか値の変更を行いません。それ以外の箇所は読み込みだけですから、都度 `unsafe` をつけるのは抵抗がありました。学習途中で恐縮ですが、C++の `const_cast` のようなものがあったり、遅延して初期化することができれば便利だと思いました(できるかも知れません)。 丁寧な解説ありがとうございました。
tatsuya6502

2021/02/01 10:08

> 学習途中で恐縮ですが、C++の `const_cast` のようなものがあったり、遅延して初期化することができれば便利だと思いました(できるかも知れません) 現時点のRustの言語本体の機能や標準ライブラリだけでは、そのようなことは実現できないです。 once_cellには任意のタイミングで1回だけ初期化できる値(OnceCell<T>型)と、初めてアクセスされたときに初期化される値(Lazy<T>型)があるわけですが、将来、これらと同等のAPIを標準ライブラリに取り込むことを検討しているようです。 https://crates.io/crates/once_cell > The API of once_cell is being proposed for inclusion in std. また、もし値を何度も書き換える必要があったら回答で紹介したstatic + Mutexなどを使ってください。static mutを使用した場合、RustコンパイラのバックエンドであるLLVMが行う最適化によって、未定義動作に繋がるコードが生成されることがあります。static mutを正しく使うのはほぼ不可能という人もいて、廃止が検討されています。 https://github.com/rust-lang/rust/issues/53639
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問