Rustの変数の基本をマスター!不変変数と可変変数の違い、設計思想を徹底解説
生徒
「Rustを勉強し始めたのですが、変数を宣言するときにletだけで書く場合と、let mutと書く場合がありますよね。これってどう使い分ければいいんですか?」
先生
「Rustでは、標準の状態だと変数は『書き換えられない(不変)』ようになっています。あえて変更したいときだけmutを付けて『可変』にするという仕組みなんです。これがRustのメモリ安全や並行プログラミングを支える重要な要素なんですよ。」
生徒
「他のプログラミング言語だと、変数はいつでも書き換えられるのが当たり前だと思っていました。なぜRustはわざわざ不変にこだわっているんですか?」
先生
「バグを未然に防ぐためです。値が勝手に変わらないことが保証されていれば、プログラムの動きを予測しやすくなりますからね。今回はこのイミュータブル(不変)とミュータブル(可変)の設計思想と使い分けを詳しく見ていきましょう!」
1. Rustの変数はデフォルトで不変である理由
Rustにおける変数の最大の特徴は、「デフォルトで不変(Immutable)」であることです。通常、多くのプログラミング言語では変数を定義した後に自由に値を上書きできますが、Rustでlet x = 5;と宣言した場合、その後でx = 6;と代入しようとするとコンパイルエラーが発生します。
なぜこのような設計になっているのでしょうか。それは、プログラムの堅牢性を高めるためです。複数の箇所から参照されるデータが、いつの間にかどこかで書き換えられてしまうと、予期せぬ挙動やデバッグの困難なバグを引き起こします。特に、大規模な開発や複雑なシステムにおいて、「この変数は絶対に値が変わらない」という確信が持てることは、エンジニアにとって大きな安心感に繋がります。
Rustのコンパイラは非常に厳格であり、開発者が意図しないデータの変更を厳しくチェックします。これにより、実行時のエラーを未然に防ぎ、高品質なソフトウェアを構築することが可能になります。
2. 不変変数(Immutable Variables)の基本と動作
まずは、Rustの基本である不変変数について詳しく見ていきましょう。不変変数は、一度値を束縛(Bind)すると、その値を変更することができません。これは、定数に近い性質を持ちますが、実行時に値が決まるという点で定数とは異なります。
不変変数を使用することで、コードの可読性が向上します。関数の途中で変数の値が変わらないことが分かっていれば、コードを追う際にかかる脳の負荷を減らすことができるからです。以下のコード例で、不変変数がどのように扱われるかを確認してみましょう。
fn main() {
let price = 1000;
println!("商品の価格は {} 円です。", price);
// 下記のコメントアウトを外すとコンパイルエラーになります
// price = 1200;
}
上記のコードで、コメントアウトされている部分を有効にすると、Rustのコンパイラは「cannot assign twice to immutable variable(不変変数に二度代入することはできません)」という明確なエラーメッセージを出してくれます。これが、Rustが安全だと言われる理由の一つです。
3. 可変変数(Mutable Variables)の使い方とmutキーワード
もちろん、プログラムを書いていると、変数の値を後から変更したい場面は多々あります。例えば、ループの中でカウンタを増やしたり、ユーザーの入力に応じてステータスを更新したりする場合です。そのためにRustでは、mutキーワードが用意されています。
let mutと宣言することで、その変数は「可変(Mutable)」になり、値を再代入することができるようになります。開発者は「この変数は変更される可能性がある」という意図を明示的に示す必要があります。これにより、コードを読む他の開発者に対しても、どのデータが変化し、どのデータが固定されているのかを伝えるメッセージになります。
fn main() {
let mut score = 0;
println!("初期スコア: {}", score);
score = 10;
println!("ボーナス獲得後のスコア: {}", score);
score += 5;
println!("最終スコア: {}", score);
}
実行結果は以下のようになります。
初期スコア: 0
ボーナス獲得後のスコア: 10
最終スコア: 15
このように、mutを付けることで柔軟なデータ操作が可能になりますが、必要以上に多用しないことがRustらしいコード(Idiomatic Rust)を書く秘訣です。
4. 不変と可変の設計思想とメモリ安全性の関係
Rustが不変性を重視する背景には、メモリ安全性(Memory Safety)とデータ競合(Data Race)の防止という深い理由があります。現代のコンピュータはマルチコアが一般的であり、複数のスレッドから同時にデータにアクセスすることが頻繁にあります。
もし、複数のスレッドがある一つの変数を同時に書き換えようとした場合、どちらの書き込みが優先されるか分からず、データが壊れてしまうことがあります。しかし、変数がデフォルトで不変であれば、複数のスレッドから同時に読み取っても安全です。なぜなら、値が変わる心配がないからです。
Rustの所有権システムと不変性のルールは、並行処理におけるバグをコンパイルレベルで排除します。可変にする必要がある場合でも、Rustは「一度に一つの場所からしか書き換えられない」といった厳しい制約を設けることで、安全性を担保しています。この設計思想により、開発者は実行時のクラッシュを恐れることなく、高速なシステムを構築できるのです。
5. シャドーイング(Shadowing)を活用したスマートな変数管理
Rustには、同じ名前の変数を再度宣言できる「シャドーイング(Shadowing)」という便利な機能があります。これは、可変変数を使う代わりの手段として非常によく使われます。
シャドーイングを使うと、以前に宣言した変数と同じ名前で新しい変数を定義できます。これにより、古い変数は「隠され(Shadowed)」、新しい値や異なる型を持たせることができます。可変変数(mut)との大きな違いは、変数の型を変更できる点と、一度定義した後は再び不変に戻るという点です。
fn main() {
let spaces = " "; // 文字列型の空白
let spaces = spaces.len(); // 数値型として再定義(シャドーイング)
println!("空白の数は {} 個です。", spaces);
}
もしこれをmutで行おうとすると、型を変更することができないためエラーになります。シャドーイングを使えば、一時的な加工が必要なデータを、同じ分かりやすい名前で扱いながら、不変性を維持したまま処理を進めることができます。これはRustプログラミングにおいて非常に洗練されたテクニックです。
6. 実践的な使い分け:どちらを使うべきか?
初心者の方が悩むのは、「いつmutを使い、いつ不変のままにするか」という点でしょう。結論から言うと、「基本はすべて不変(let)で書き、コンパイラに怒られたり、どうしても変更が必要な場合だけ可変(let mut)にする」のがベストプラクティスです。
以下に、判断基準となるチェックリストをまとめました。
- データの計算途中で、元の値を保持しておく必要がない場合はシャドーイングを検討する。
- 大規模なデータ構造(ベクトルやハッシュマップ)に要素を順次追加していく場合は
mutを使用する。 - 関数の引数として渡された値を変更する必要がある場合は、シグネチャに
mutを検討する。 - ループの制御変数など、本質的に状態が変わるものは
mutにする。
Rustのコンパイラは親切なので、mutを付けたのに一度も値を変更していない場合、「このmutは不要ですよ」という警告(Warning)を出してくれます。この警告に従ってコードを修正していくだけでも、自然と綺麗なRustコードが書けるようになります。
7. 定数(Constants)と不変変数の違い
不変変数と似たものに、定数(Constants)があります。Rustではconstキーワードを使用して宣言します。一見すると不変変数と同じに見えますが、明確な違いがあります。
まず、定数は常に型を明示する必要があります。また、定数はプログラムの実行期間中ずっと有効であり、グローバルスコープでも宣言可能です。最も大きな違いは、定数は「コンパイル時に計算可能な値」でなければならないという点です。一方で不変変数は、関数の戻り値など、実行時に初めて決まる値を保持することができます。
const MAX_POINTS: u32 = 100_000;
fn main() {
let current_time = std::time::Instant::now(); // 実行時に決まるのでconstは不可
println!("最大ポイント: {}", MAX_POINTS);
}
設定値や魔法の数字(マジックナンバー)を避けるために定数を使用し、ロジックの中でのデータの受け渡しには変数を使用するという使い分けが一般的です。このように、用途に合わせて適切な「箱」を選ぶことが、バグのないプログラミングへの第一歩となります。
8. Rustのデータ型とメモリ管理の深掘り
変数の可変性を語る上で欠かせないのが、データ型とメモリの関係です。Rustには「スカラ型(整数、浮動小数点、布飾、文字)」と「複合型(タプル、配列)」があります。これらのデータがメモリ上のどこに配置されるか(スタックかヒープか)によって、コピーの挙動や所有権の移動が変わります。
例えば、数値型のような単純なデータはスタックに置かれ、コピーが容易です。しかし、String型のような可変長のデータはヒープに置かれます。不変変数としてStringを宣言した場合、その文字列の内容自体を変更することはできませんが、所有権を別の変数に移動させることは可能です。
不変性は、単に「値が変わらない」というだけでなく、メモリ上の参照が安全であることを保証する基盤です。この基本を理解することで、Rustの最難関と言われる「所有権(Ownership)」や「借用(Borrowing)」の概念もスムーズに理解できるようになります。変数の扱いに慣れることは、Rustという強力な言語を自由自在に操るための鍵なのです。
9. コンパイラとの対話で学ぶ変数の性質
Rustの学習において最も効率的な方法は、わざとコンパイルエラーを起こしてみることです。不変変数に値を代入しようとしたり、mutを付け忘れてメソッドを呼び出したりしたときに、Rustのコンパイラがどのようなアドバイスをくれるかを確認してください。
Rustのメッセージは、他の言語と比較しても非常に親切で具体的です。「ここにmutを付ければ解決しますよ」といった具体的な修正案を提示してくれることも珍しくありません。このコンパイラとの対話を通じて、変数のスコープやライフタイム、そして可変性の重要性を体得していくことができます。最初は厳しさに戸惑うかもしれませんが、それはコンパイラがあなたの代わりにバグを見つけてくれている証拠なのです。
Rustの変数の仕組みを正しく理解し、適切に使い分けることで、高速で、メモリ効率が良く、そして何よりも安全なアプリケーションを開発できるようになります。一歩ずつ、Rustの世界を楽しんでいきましょう。