問題点PythonやJavaScriptのような言語に慣れていると、単にコンストラクタを呼び出すだけでグローバルな設定用マップを初期化できると期待するかもしれません。しかし、Rustはコンパイルプロセスに対して非常に慎重です。Rustでは、コードのビルド時に起こることと、実行時に起こることを厳格に区別します。
以下は、よくこの問題を引き起こすコードスニペットです:
use std::collections::HashMap;
// これは E0015 を発生させます
const MY_CONFIG: HashMap = HashMap::new();
fn main() {
println!("{:?}", MY_CONFIG);
}
cargo buildを実行すると、コンパイラは次のメッセージを表示して停止します:
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
--> src/main.rs:4:39
|
4 | const MY_CONFIG: HashMap = HashMap::new();
| ^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants
なぜこれが起こるのかconstキーワードは、コンパイル時に一度だけ値を計算し、それが使用されるすべての場所にインライン展開するようRustに指示します。これを安全に行うために、コンパイラはconstブロック内で呼び出されるすべての関数がconst fnとしてマークされていることを要求します。これらは、プログラム全体を実行することなくコンパイラが実行できる決定論的な関数です。
なぜHashMap::new()がconst fnではないのか不思議に思うかもしれません。その理由は、HashMapがDoS攻撃を防ぐためのハッシュアルゴリズムにランダムシードを使用しているからです。そのランダム性を生成するにはオペレーティングシステムとのやり取りが必要ですが、コンパイラはバイナリのビルド中にそれを行うことができません。
解決策1:LazyLockを使用する(モダンな標準)Rust 1.80以降を使用している場合、std::sync::LazyLockが最適です。これを使用すると、最初にアクセスした瞬間まで初期化を待機するグローバル変数を定義できます。これにより、スレッド安全性を維持しながらコンパイル時の制限を回避できます。
use std::collections::HashMap;
use std::sync::LazyLock;
// LazyLockはMY_CONFIGが最初に使用されたときにのみ初期化される
static MY_CONFIG: LazyLock> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("api_version", 2);
m.insert("timeout_ms", 5000);
m
});
fn main() {
// HashMapは実際にはここで作成される
println!("Timeout: {:?}", MY_CONFIG.get("timeout_ms"));
}
constからstaticに切り替えたことに注意してください。これにより、データが至る所にコピーされるのではなく、単一の固定されたメモリ位置に保持されるようになります。
解決策2:動的なデータにOnceLockを使用するプログラムが実行されるまでグローバル変数の値がわからない場合があります。例えば、環境変数からDATABASE_URLを読み込むような場合です。このようなケースでは、std::sync::OnceLockがより良い選択肢となります。
use std::collections::HashMap;
use std::sync::OnceLock;
static APP_CACHE: OnceLock> = OnceLock::new();
fn main() {
// 実行時に一度だけキャッシュを初期化する
let cache = APP_CACHE.get_or_init(|| {
let mut m = HashMap::new();
m.insert("session_id".to_string(), "abc-123".to_string());
m
});
println!("Current session: {}", cache["session_id"]);
}
解決策3:独自の関数を'const'に変換する独自の関数を呼び出しているときにE0015が発生した場合は、const修飾子を追加することで解決できることがよくあります。これは、関数がヒープ割り当てや複雑なI/Oを実行しない限り機能します。
// 'const'を追加することで、コンパイル中に実行できるようになる
const fn get_max_threads(cores: u32) -> u32 {
cores * 2
}
const WORKER_LIMIT: u32 = get_max_threads(8);
fn main() {
println!("Max workers: {}", WORKER_LIMIT);
}
解決策4:オーバーヘッドゼロの静的配列を使用する小規模で固定されたルックアップの場合、HashMapは過剰かもしれません。タプルの静的配列の方が高速で、コンパイラにとっても扱いやすいことがよくあります。このアプローチでは、実行時の初期化時間はゼロです。
// 定数には単純なペアの配列で十分なことが多い
const SERVER_SETTINGS: [(&str, u16); 3] = [
("web", 8080),
("api", 9000),
("metrics", 9100),
];
fn main() {
for (service, port) in SERVER_SETTINGS {
println!("Service {} is on port {}", service, port);
}
}

