letで始まるRustの変数宣言

Rustで変数を作るにはletキーワードを使います。

fn main() {
    let x = 42;
    println!("{}", x); // 42
}

コンパイラが右辺の値から型を推論するため、多くの場合は型注釈を省略できます。明示的に型を指定する場合はlet x: i32 = 42;のように書きます。

型推論が効かないケースと型注釈

型推論だけでは型が確定しない場面があります。

fn main() {
    // parse()の戻り値型が曖昧 → 型注釈が必要
    let guess: u32 = "42".parse().expect("数値ではありません");
    println!("{}", guess);
}

parse()のように複数の型を返しうるメソッドでは、型注釈を省略するとコンパイルエラーになります。型注釈を書くことで「どの型として解釈するか」をコンパイラに伝えます。

変数名の命名規則

Rustでは変数名にスネークケースsnake_case)を使うのが慣例です。キャメルケースで書いてもコンパイルは通りますが、コンパイラが警告を出します。

fn main() {
    let user_name = "田中"; // OK: スネークケース
    let userName = "田中";  // 警告: should have a snake case name
    println!("{} {}", user_name, userName);
}

先頭に_を付けると「意図的に未使用」という意味になり、未使用変数の警告を抑制できます。

fn main() {
    let _unused = 100; // 未使用でも警告なし
}

不変がデフォルト ── Rustが再代入を拒む理由

Rustの変数は**デフォルトで不変(immutable)**です。letで宣言した変数に別の値を代入しようとするとコンパイルエラーになります。

fn main() {
    let x = 5;
    x = 10; // コンパイルエラー: cannot assign twice to immutable variable
}

C、Python、JavaScriptなど多くの言語では変数はデフォルトで再代入可能です。Rustがあえて不変をデフォルトにしているのは、次の2つの安全性を重視しているためです。

  • バグの予防: 意図しない値の上書きをコンパイル時点で検出できます
  • 並行処理の安全性: 不変な値は複数スレッドから安全に読み取れます

mutで可変にする

再代入が必要な場面ではmutキーワードを付けて可変変数として宣言します。

fn main() {
    let mut counter = 0;
    counter += 1;
    counter += 1;
    println!("counter = {}", counter); // counter = 2
}

mutを付けることで「この変数は途中で値が変わる」という意図がコードに明示されます。コードを読む側にとっても、mutの有無で変数の振る舞いが予測しやすくなります。

mutを使う場面の判断基準

実務では次のように使い分けるのが一般的です。

場面方針
ループカウンタ・累算器let mutを使用
関数の引数を加工して返す新しい変数にシャドーイングで再束縛
一度だけ値を決定するletのみ(不変)
構造体のフィールドを更新let mutで構造体ごと可変にする

Rustでは構造体の一部フィールドだけを可変にはできません。構造体を可変にする場合、let mutでインスタンス全体を可変にします。

シャドーイング ── 同名変数の再束縛

Rustでは同じスコープ内で同名の変数をletで再宣言できます。これをシャドーイングと呼びます。

fn main() {
    let x = 5;
    let x = x + 1;   // 元のxをシャドーイング → 6
    let x = x * 2;   // さらにシャドーイング → 12
    println!("{}", x); // 12
}

シャドーイングはmutによる再代入とは根本的に異なります。

シャドーイングとmutの違い

fn main() {
    // シャドーイング: 型を変更できる
    let data = "hello";
    let data = data.len(); // &str → usize に型が変わる
    println!("{}", data);  // 5

    // mut: 型は変更できない
    let mut value = "hello";
    // value = value.len(); // コンパイルエラー: 型が合わない
}
観点シャドーイング (letの再宣言)mutによる再代入
型の変更可能不可
新しい変数が作られるかはい(元の変数は隠される)いいえ(同じメモリ領域を更新)
再代入後の不変性再び不変にできるmutが付いている限り可変
主な用途型変換、中間値の段階的加工カウンタ、バッファへの追記

スコープとシャドーイング

ブロック内でシャドーイングした変数は、ブロックを抜けると元の変数が復活します。

fn main() {
    let x = 10;
    {
        let x = x + 5; // 内側のスコープでシャドーイング
        println!("内側: {}", x); // 15
    }
    println!("外側: {}", x); // 10(元のxが有効)
}

この動作はRustのブロックスコープの仕組みによるもので、内側のlet xは外側のxとは別の変数です。

const ── コンパイル時に確定する定数

constはコンパイル時に値が確定する定数を宣言するキーワードです。letとの違いがいくつかあります。

const MAX_CONNECTIONS: u32 = 100;
const PI: f64 = 3.141592653589793;

fn main() {
    println!("最大接続数: {}", MAX_CONNECTIONS);
    println!("円周率: {}", PI);
}

constの制約

  • 型注釈が必須: const X = 10;はコンパイルエラーになります
  • mutを付けられない: 定数は常に不変です
  • コンパイル時定数式のみ: 関数の戻り値やランタイムに決まる値は代入できません
  • 命名規則: SCREAMING_SNAKE_CASE(全大文字+アンダースコア区切り)が慣例です

let(不変変数)とconstの使い分け

letで不変にした変数」とconstは、どちらも値を変更できないという点では同じに見えます。しかし用途は異なります。

観点let(不変変数)const
値の決定タイミング実行時でも可コンパイル時のみ
スコープブロックスコープ宣言された任意のスコープ(グローバル可)
型注釈省略可能(型推論)必須
シャドーイング可能不可
インライン展開されないコンパイラが定数をインライン展開する場合がある

設定値や数学的定数など「プログラムの実行を通じて絶対に変わらない値」にはconstを使い、実行時に計算される値にはletを使うのが適切です。

static ── プログラム全体で生存する変数

static変数はプログラムの実行開始から終了まで存在し続けます。

static LANGUAGE: &str = "Rust";

fn main() {
    println!("{}", LANGUAGE); // Rust
}

static mutの危険性

static mutで可変なグローバル変数を作ることもできますが、マルチスレッド環境でデータ競合を引き起こす可能性があるためunsafeブロック内でしかアクセスできません。

static mut COUNTER: u32 = 0;

fn increment() {
    unsafe {
        COUNTER += 1;
    }
}

fn main() {
    increment();
    increment();
    unsafe {
        println!("COUNTER = {}", COUNTER); // COUNTER = 2
    }
}

実務で可変なグローバル状態が必要な場合は、static mutの代わりにスレッドセーフな手段を使うのが定石です。

グローバルな可変状態の安全な実装方法

static mutを避ける代表的な方法を2つ紹介します。

std::sync::Mutexを使う方法:

use std::sync::Mutex;

static COUNTER: Mutex<u32> = Mutex::new(0);

fn main() {
    *COUNTER.lock().unwrap() += 1;
    *COUNTER.lock().unwrap() += 1;
    println!("COUNTER = {}", COUNTER.lock().unwrap()); // COUNTER = 2
}

std::sync::atomicを使う方法(整数型向け):

use std::sync::atomic::{AtomicU32, Ordering};

static COUNTER: AtomicU32 = AtomicU32::new(0);

fn main() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
    COUNTER.fetch_add(1, Ordering::SeqCst);
    println!("COUNTER = {}", COUNTER.load(Ordering::SeqCst)); // COUNTER = 2
}

Mutexは任意の型に使えますが、ロックのオーバーヘッドがあります。整数のカウンタであればAtomicU32のようなアトミック型のほうが軽量です。

const・static・letの全体比較

観点letconststatic
値の決定実行時コンパイル時コンパイル時
生存期間スコープ内インライン展開プログラム全体('static
メモリ上の実体スタック実体なし(展開される)固定アドレス
可変化mut不可static mut(unsafe必須)
グローバル宣言不可
用途通常の変数定数値グローバル状態・設定

Rustの基本データ型と変数

変数に格納する値の型を理解しておくことで、型注釈を書く際に迷わなくなります。

スカラー型

分類サイズ値の範囲
符号付き整数i8, i16, i32, i64, i1281〜16バイト−2^(n−1) 〜 2^(n−1)−1
符号なし整数u8, u16, u32, u64, u1281〜16バイト0 〜 2^n−1
ポインタサイズ整数isize, usizeアーキテクチャ依存配列のインデックスに使用
浮動小数点f32, f644, 8バイトIEEE 754 準拠
ブール値bool1バイトtrue / false
文字char4バイトUnicode スカラー値

デフォルトの整数型はi32、浮動小数点型はf64です。

fn main() {
    let a = 42;       // i32(デフォルト)
    let b = 3.14;     // f64(デフォルト)
    let c = true;     // bool
    let d = 'あ';     // char(Unicodeに対応)
    let e: u8 = 255;  // 明示的に型指定
    println!("{} {} {} {} {}", a, b, c, d, e);
}

複合型

fn main() {
    // タプル: 異なる型をまとめる
    let point: (f64, f64) = (1.5, 3.2);
    let (x, y) = point; // 分配束縛で取り出す
    println!("x={}, y={}", x, y);

    // 配列: 同じ型・固定長
    let months = ["1月", "2月", "3月"];
    println!("最初の月: {}", months[0]);

    // 配列の型注釈: [要素型; 要素数]
    let zeros: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
    println!("{:?}", zeros);
}

タプルはパターンマッチ(分配束縛)やインデックスアクセス(point.0)で要素を取り出せます。配列は固定長で、可変長のコレクションが必要な場合はVec<T>を使います。

所有権と変数の関係

Rustの変数を語るうえで避けて通れないのが**所有権(ownership)**です。すべての値には「所有者」となる変数が1つだけ存在し、所有者がスコープを抜けるとその値は自動的に解放されます。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有権がs1からs2にムーブ
    // println!("{}", s1); // コンパイルエラー: s1はもう使えない
    println!("{}", s2);    // OK
}

ムーブとコピー

スタックに格納される型(整数、浮動小数点、boolchar、それらのみで構成されるタプル)はCopyトレイトを実装しており、代入時にムーブではなくコピーが発生します。

fn main() {
    let a = 5;
    let b = a;     // i32はCopyなのでコピーされる
    println!("a={}, b={}", a, b); // 両方使える

    let s1 = String::from("hello");
    let s2 = s1;   // Stringはムーブされる
    // println!("{}", s1); // エラー: 所有権はs2に移動済み
    println!("{}", s2);
}

参照と借用

所有権を移動させずに値を利用するには**参照(&を使います。参照を渡すことを借用(borrowing)**と呼びます。

fn print_length(s: &String) {
    println!("長さ: {}", s.len());
}

fn main() {
    let message = String::from("Rustの変数");
    print_length(&message);   // 借用(所有権は移動しない)
    println!("{}", message);   // まだ使える
}

可変参照(&mut)を使えば、借用先で値を変更することもできます。ただし、可変参照は同時に1つしか存在できないという制約があります。

fn add_exclamation(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut greeting = String::from("Hello");
    add_exclamation(&mut greeting);
    println!("{}", greeting); // Hello!
}

よくあるコンパイルエラーと対処法

Rustの変数に関するコンパイルエラーは、慣れないうちは頻出します。代表的なエラーとその対処法を整理します。

E0384: 不変変数への再代入

error[E0384]: cannot assign twice to immutable variable `x`

原因: let xで宣言した変数に再代入しようとしています。

対処法: let mut xに変更するか、let x = 新しい値;でシャドーイングします。

E0308: 型の不一致

error[E0308]: mismatched types
  expected `i32`, found `&str`

原因: mut変数に異なる型の値を代入しようとしています。

対処法: シャドーイング(letで再宣言)を使えば型を変更できます。mutは同じ型への再代入のみ可能です。

E0382: ムーブ後の変数使用

error[E0382]: borrow of moved value: `s`

原因: 所有権がムーブした変数を使おうとしています。

対処法: 参照(&s)で借用するか、.clone()で値を複製します。

他言語との比較で理解するRustの変数

他の言語を経験している方向けに、変数の振る舞いの違いを整理します。

言語デフォルトの可変性定数宣言型推論再宣言(シャドーイング)
Rust不変(letconstあり可(型変更も可)
Python可変慣例のみ(定数構文なし)動的型付け可(動的型)
JavaScriptletは可変 / constは不変const動的型付け同一スコープでは不可
Go可変constあり(:=同一スコープでは不可
C++可変const / constexprauto同一スコープでは不可

Rustのletは他言語のconstに近い性質を持っています。JavaScriptのconstはオブジェクトの中身を変更できますが、Rustのletはプリミティブ型もヒープ型も一律で再代入を禁止します。

まとめ

Rustの変数は「不変がデフォルト」という原則を軸に、mut・シャドーイング・conststaticという4つの仕組みを組み合わせて使います。

  • let: 基本の変数宣言。不変がデフォルトで安全性が高い
  • let mut: 再代入が必要な場面で使用。カウンタやバッファに適している
  • シャドーイング: 型変換や段階的な値加工に適している。mutより意図が明確になる場面がある
  • const: コンパイル時定数。設定値や数学的定数に使用
  • static: プログラム全体で生存する変数。可変にする場合はMutexやアトミック型を推奨

所有権・借用・ライフタイムはRustの変数の振る舞いと密接に結びついています。変数の基本を押さえたうえでこれらの概念を学ぶと、Rustのメモリ管理の設計思想が見えてきます。