Day 001 — 変数・可変性・シャドーイング

2026-07-29 🟢 初心者 / Phase 1 概念理解→実装 変数・可変性・シャドーイング

📚 背景知識(読んでから問題へ)

Rustの変数はデフォルトで不変(immutable)です。多くの言語(Go/Java/TypeScript等)では変数は基本的に可変で、不変にしたいときだけfinalconstを付けますが、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にはない強みとして得られます。

  1. 型を変更できる&stri32 など)。mutでは型は固定のままです。
  2. シャドーイング後の新しい束縛も、明示しない限りやはり不変です。つまり「一時的に加工して、以降は書き換えさせたくない」という意図をそのまま表現できます。

シャドーイングは「同じ意味を持つ値を、名前を変えずに段階的に加工していく」パターン(例: 文字列の前後の空白除去 → パース → 数値変換)でよく使われます。input, input_trimmed, input_parsedのような名前の氾濫を防ぎ、コードの意図(同じ概念の異なる表現)を保ったまま読みやすさを維持できます。

📝 問題

タスク1(概念理解):
以下の問いに簡潔に答えてください。

  1. letlet mutの違いは何ですか。Rustが変数をデフォルトで不変にしている設計上の理由も含めて説明してください。
  2. シャドーイングとmutによる再代入は何が違いますか。「型を変更できるか」という観点を含めて説明してください。
  3. 以下のコードはコンパイルエラーになります。どのようなエラーメッセージが出るか予想し、修正方法を答えてください。
fn main() {
    let x = 5;
    x = 6;
    println!("{}", x);
}

タスク2(実装):
以下の要件を満たすmain.rsを実装してください。

  1. 不変変数language"Rust"を束縛し、出力する
  2. mutを使って可変変数counterを0で初期化し、forループで5回インクリメントしてから出力する
  3. 文字列" 42 "を、シャドーイングを使って「前後の空白除去(&str)→ i32へのパース(型変更)→ 2倍した値」の順に段階的に加工し、同じ変数名inputのまま最終結果を出力する
  4. 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 解説

1
letで不変変数を宣言する
let language = "Rust";
mutを付けないletは不変束縛です。以降のコードでlanguageに再代入しようとすると、コンパイル時にエラーになります。「後から変わらない」ことがコード上で保証されるため、読み手はこの値を安心して追跡できます。
2
let mutで可変変数を宣言する
let mut counter = 0;
for _ in 0..5 {
    counter += 1;
}
ループの中で値を更新する必要があるため、mutを付けます。mutはあくまで「同じ型の値を、同じメモリ位置で書き換える」ことの許可であり、counterの型がi32から他の型に変わることはありません。
3
シャドーイングで型を変えながら段階的に加工する
let input = "   42   ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
let input = input * 2;
inputという名前を保ったまま、&str(空白あり)→ &str(空白なし)→ i32(パース後)→ i32(2倍後)と型・値を変化させています。mutではこのうち「型を変える」ステップ(&stri32)が実現できません。それぞれのletが独立した新しい変数を作っているため、型が変わっても問題なくコンパイルが通ります。
4
スコープとシャドーイングの関係を確認する
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]: cannot assign twice to immutable variable `x` --> src/main.rs:3:5 | 2 | let x = 5; | - first assignment to `x` 3 | x = 6; | ^^^^^ cannot assign twice to immutable variable | help: consider making this binding mutable | 2 | let mut x = 5; | +++

読み方:

  • 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;が許可されコンパイルが通ります。

🌐 他言語との比較

観点RustGoJavaTypeScript
デフォルトの可変性不変(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でシャドーイングする必要がある

🚀 次のステップ

  • 発展: constletの違い(コンパイル時定数であること・型注釈必須であること・グローバルスコープで使えること)を実際にコードで確かめてみましょう。また、ブロック({})が値を返せる性質(let y = { let x = 3; x + 1 };)と組み合わせたシャドーイングパターンにも触れておくと理解が深まります
  • 次回予告: Day 002 — 基本型とデータ型(スカラー型・複合型(タプル・配列)の使い分け)

🎯 自己評価

自分の回答

気づき・メモ