📚 背景知識(読んでから問題へ)
Rustの変数はデフォルトで不変(immutable)です。多くの言語(Go/Java/TypeScript等)では変数は基本的に可変で、不変にしたいときだけfinalやconstを付けますが、Rustはこの前提を逆転させています。これは「意図しない変更によるバグ」をコンパイル時に排除するというRustの設計思想(所有権システムによる安全性の型システムへの落とし込み)の一部です。可変にしたい場合は明示的にmutキーワードを付ける必要があり、コードを読むだけで「この変数は後から変わるかもしれない」という箇所が一目でわかります。
let による不変束縛(デフォルト)
let x = 5;
// x = 6; // コンパイルエラー: cannot assign twice to immutable variable `x`
let mut による可変束縛
let mut x = 5;
x = 6; // OK
mutを付けると、同じメモリ位置に対して再代入できるようになります。ただし型は変えられません(あくまで同じ型の値を上書きするだけです)。
シャドーイング(shadowing)
let x = 5;
let x = x + 1; // 新しい束縛。xという名前は同じだが別の変数
let x = "six"; // 型すら変更できる
シャドーイングはmutとは全く別の仕組みです。letを同じ変数名で再度使うと、コンパイラは「新しい変数」を作り、古い変数を覆い隠します。これにより次の2点がmutにはない強みとして得られます。
- 型を変更できる(
&str→i32など)。mutでは型は固定のままです。 - シャドーイング後の新しい束縛も、明示しない限りやはり不変です。つまり「一時的に加工して、以降は書き換えさせたくない」という意図をそのまま表現できます。
シャドーイングは「同じ意味を持つ値を、名前を変えずに段階的に加工していく」パターン(例: 文字列の前後の空白除去 → パース → 数値変換)でよく使われます。input, input_trimmed, input_parsedのような名前の氾濫を防ぎ、コードの意図(同じ概念の異なる表現)を保ったまま読みやすさを維持できます。
📝 問題
タスク1(概念理解):
以下の問いに簡潔に答えてください。
letとlet mutの違いは何ですか。Rustが変数をデフォルトで不変にしている設計上の理由も含めて説明してください。- シャドーイングと
mutによる再代入は何が違いますか。「型を変更できるか」という観点を含めて説明してください。 - 以下のコードはコンパイルエラーになります。どのようなエラーメッセージが出るか予想し、修正方法を答えてください。
fn main() {
let x = 5;
x = 6;
println!("{}", x);
}
タスク2(実装):
以下の要件を満たすmain.rsを実装してください。
- 不変変数
languageに"Rust"を束縛し、出力する mutを使って可変変数counterを0で初期化し、forループで5回インクリメントしてから出力する- 文字列
" 42 "を、シャドーイングを使って「前後の空白除去(&str)→i32へのパース(型変更)→ 2倍した値」の順に段階的に加工し、同じ変数名inputのまま最終結果を出力する let x = 5;のあと、{}で囲んだ内側スコープでxをシャドーイングして2倍した値を出力し、スコープを抜けた後の外側のxが元の値のままであることを出力して確認する
🔍 ヒント(段階的開示)
ヒント1 — 方向性
- Rustでは変数はデフォルトで不変です。可変にしたい場合は
let mutを使います - シャドーイングは
letを同じ変数名でもう一度使うだけです。特別な構文は不要です - 型を変えたい場合は
mutでは実現できません。シャドーイングが必須です
ヒント2 — アプローチ
- 文字列の前後の空白除去には
str::trim()メソッドが使えます(&strを返す) - 文字列から数値へのパースは
.parse()を使い、型注釈またはlet x: i32 = ...でターゲット型を明示します。戻り値はResultなので.unwrap()で値を取り出せます(今回は失敗しない前提でOKです) - ループは
for _ in 0..5 { ... }の形式で書けます - スコープは
{ }で自由に作れます。内側のletは内側スコープでのみ有効です
ヒント3 — コード骨格
fn main() {
// 1. 不変変数
let language = "Rust";
println!("言語: {}", language);
// 2. mutによるカウンタ
let mut counter = 0;
for _ in 0..5 {
// counterをインクリメント...
}
println!("カウンタ: {}", counter);
// 3. シャドーイングによる段階的加工
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
// 2倍する処理...
// 4. スコープとシャドーイング
let x = 5;
{
// 内側スコープでxをシャドーイング...
}
// 外側のxを出力...
}
✅ 模範解答
fn main() {
// --- 1. 不変変数(デフォルト) ---
let language = "Rust";
println!("--- 不変変数 ---");
println!("言語: {}", language);
// --- 2. mut による可変変数(カウンタ) ---
let mut counter = 0;
for _ in 0..5 {
counter += 1;
}
println!("--- mut によるカウンタ ---");
println!("カウンタ: {}", counter);
// --- 3. シャドーイングによる段階的加工 ---
let input = " 42 "; // &str(前後に空白あり)
let input = input.trim(); // &str -> &str(空白除去)
let input: i32 = input.parse().unwrap(); // &str -> i32(型変更)
let input = input * 2; // i32 -> i32(再計算)
println!("--- シャドーイングの結果 ---");
println!("input: {}", input);
// --- 4. スコープとシャドーイング ---
let x = 5;
{
let x = x * 2; // 内側スコープだけの新しい束縛
println!("--- スコープ内シャドーイング ---");
println!("内側スコープの x: {}", x);
}
println!("外側スコープの x: {}", x);
}
▶ 実行結果を見る(cargo run)
--- 不変変数 ---
言語: Rust
--- mut によるカウンタ ---
カウンタ: 5
--- シャドーイングの結果 ---
input: 84
--- スコープ内シャドーイング ---
内側スコープの x: 10
外側スコープの x: 5
🪜 Step-by-Step 解説
letで不変変数を宣言するlet language = "Rust";
mutを付けないletは不変束縛です。以降のコードでlanguageに再代入しようとすると、コンパイル時にエラーになります。「後から変わらない」ことがコード上で保証されるため、読み手はこの値を安心して追跡できます。
let mutで可変変数を宣言するlet mut counter = 0;
for _ in 0..5 {
counter += 1;
}
ループの中で値を更新する必要があるため、mutを付けます。mutはあくまで「同じ型の値を、同じメモリ位置で書き換える」ことの許可であり、counterの型がi32から他の型に変わることはありません。
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
let input = input * 2;
inputという名前を保ったまま、&str(空白あり)→ &str(空白なし)→ i32(パース後)→ i32(2倍後)と型・値を変化させています。mutではこのうち「型を変える」ステップ(&str → i32)が実現できません。それぞれのletが独立した新しい変数を作っているため、型が変わっても問題なくコンパイルが通ります。
let x = 5;
{
let x = x * 2;
println!("内側スコープの x: {}", x);
}
println!("外側スコープの x: {}", x);
内側の{}ブロックで作られたxは、そのブロックを抜けると同時に破棄(drop)されます。外側のxはシャドーイングされていた間も裏で存在し続けており、ブロックを抜けると再び見えるようになります。名前は同じでも、実体としては全く別の2つの変数です。
💡 設計思想・なぜこう書くのか
mutで明示させることで、バグの起きやすい「状態が変化する箇所」をコード全体から視覚的に絞り込めます。これはRustが掲げる「恐れなき並行性(fearless concurrency)」の土台の一つでもあります(可変でなければデータ競合はそもそも起こり得ません)。mutと別に用意されている理由: mutは「同じ型の値の再代入」を許可する仕組み、シャドーイングは「新しい意味を持つ値に、同じ名前で束縛し直す」仕組みです。両者は目的が異なります。もしmutだけで型変更まで許してしまうと、同じメモリ位置・同じ変数がプログラムの途中で全く別の型として扱われることになり、型の一貫性という安全性の柱が崩れてしまいます。シャドーイングは「新しい変数を作る」という形を取ることで、型システムの一貫性を保ったまま柔軟な書き方を可能にしています。🛑 コンパイルエラーが出た場合(タスク1-Q3の解答)
タスク1のQ3で提示したコードは、以下のようなrustcのエラーを出します。
読み方:
error[E0384]: エラー番号。rustc --explain E0384でより詳しい説明を読める--> src/main.rs:3:5: エラーが発生した場所(3行目5文字目)first assignment to `x`: 2行目のlet x = 5;が最初の(そして唯一許される)代入であることを示す^^^^^ cannot assign twice to immutable variable: 3行目のx = 6;が「不変変数への2度目の代入」として拒否されているhelp: consider making this binding mutable: 修正案としてmutを付けることをコンパイラ自身が提案してくれている
修正方法: 1行目をlet mut x = 5;に変更すれば、x = 6;が許可されコンパイルが通ります。
🌐 他言語との比較
| 観点 | Rust | Go | Java | TypeScript |
|---|---|---|---|---|
| デフォルトの可変性 | 不変(let)。可変にはmutが必須 | 可変(var/:=はデフォルト可変) | 可変。不変にするにはfinalが必要 | 可変(let)。constで再代入不可に |
| 同一スコープでの再宣言(シャドーイング) | 可能。型も変更可能 | 不可(コンパイルエラー) | 不可(コンパイルエラー) | 不可(SyntaxError: Identifier has already been declared) |
| ネストしたスコープでの再宣言 | 可能 | 可能(別変数として扱われる) | 不可(外側と同名は使えない) | 可能(ブロックスコープによるシャドーイング) |
| 型を変えながらの再束縛 | シャドーイングで可能 | 別の変数名が必要 | 別の変数名が必要 | 別の変数名が必要 |
Rustが際立つのは、同一スコープ内でもシャドーイングできる点です。TypeScript/JavaScriptのletはブロックスコープが変わればシャドーイングできますが、同じブロック内でlet xを2回書くとSyntaxErrorになります。Rustはこの制約がなく、「同じ名前のまま値を段階的に変換していく」というパイプライン的な書き方を、言語仕様として一級市民として扱っています。
🏆 実務での使いどころ
- 入力値のバリデーション/加工パイプライン: 環境変数やユーザー入力を「生の文字列 → トリム → パース → バリデーション済みの値」と段階的に変換する際、シャドーイングを使えば
raw_input,trimmed_input,parsed_inputのような名前の氾濫を避けられます - 設定値の読み込み:
Stringとして読み込んだ設定値を、同じ変数名のまま型付きの設定構造体やenumに変換していくコードでよく使われます mutによるカウンタ・アキュムレータ: ループでの集計処理、ビルダーパターンでの内部状態更新など、値そのものが変化していくことを表現したい場面ではmutを使います- 不変変数のレビュー効率: 大規模なプロダクションコードでは、関数の大半の変数が
mutなしの不変変数です。mutがついている箇所だけをコードレビューで重点的に確認すれば、状態変更に起因するバグを効率よく洗い出せます
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
mutを付ければシャドーイングと同じことができると思う | 他言語の「再代入」の感覚をそのまま持ち込んでしまう | mutは同じ型・同じメモリ位置への再代入のみ許可する。型を変えたい場合はmutでは実現できず、シャドーイング(letの再利用)が必要 |
| シャドーイングするとメモリリークすると誤解する | 変数名が同じままなので「古い値が消えていない」ように見える | 古い束縛はスコープを抜けた時点でdropされる。同じ型であれば多くの場合コンパイラが同じメモリ位置を再利用するよう最適化する |
不変変数(let)と定数(const)を同じものだと思う | どちらも「変更できない」という性質を持つため | letは実行時に値が決まる通常の変数(関数の戻り値なども束縛可能)。constはコンパイル時に値が確定していなければならず、型注釈が必須で、mutを付けることもできない |
| シャドーイング後の変数はそのまま自由に書き換えられると思う | シャドーイングという操作自体が「変更」のように感じられる | シャドーイングで作られた新しい束縛も、let mutと書かない限りは不変。書き換えたい場合はlet mutでシャドーイングする必要がある |
🚀 次のステップ
- 発展:
constとletの違い(コンパイル時定数であること・型注釈必須であること・グローバルスコープで使えること)を実際にコードで確かめてみましょう。また、ブロック({})が値を返せる性質(let y = { let x = 3; x + 1 };)と組み合わせたシャドーイングパターンにも触れておくと理解が深まります - 次回予告: Day 002 — 基本型とデータ型(スカラー型・複合型(タプル・配列)の使い分け)