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の全体比較
| 観点 | let | const | static |
|---|---|---|---|
| 値の決定 | 実行時 | コンパイル時 | コンパイル時 |
| 生存期間 | スコープ内 | インライン展開 | プログラム全体('static) |
| メモリ上の実体 | スタック | 実体なし(展開される) | 固定アドレス |
| 可変化 | mut可 | 不可 | static mut(unsafe必須) |
| グローバル宣言 | 不可 | 可 | 可 |
| 用途 | 通常の変数 | 定数値 | グローバル状態・設定 |
Rustの基本データ型と変数
変数に格納する値の型を理解しておくことで、型注釈を書く際に迷わなくなります。
スカラー型
| 分類 | 型 | サイズ | 値の範囲 |
|---|---|---|---|
| 符号付き整数 | i8, i16, i32, i64, i128 | 1〜16バイト | −2^(n−1) 〜 2^(n−1)−1 |
| 符号なし整数 | u8, u16, u32, u64, u128 | 1〜16バイト | 0 〜 2^n−1 |
| ポインタサイズ整数 | isize, usize | アーキテクチャ依存 | 配列のインデックスに使用 |
| 浮動小数点 | f32, f64 | 4, 8バイト | IEEE 754 準拠 |
| ブール値 | bool | 1バイト | true / false |
| 文字 | char | 4バイト | 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
}
ムーブとコピー
スタックに格納される型(整数、浮動小数点、bool、char、それらのみで構成されるタプル)は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 | 不変(let) | const | あり | 可(型変更も可) |
| Python | 可変 | 慣例のみ(定数構文なし) | 動的型付け | 可(動的型) |
| JavaScript | letは可変 / constは不変 | const | 動的型付け | 同一スコープでは不可 |
| Go | 可変 | const | あり(:=) | 同一スコープでは不可 |
| C++ | 可変 | const / constexpr | auto | 同一スコープでは不可 |
Rustのletは他言語のconstに近い性質を持っています。JavaScriptのconstはオブジェクトの中身を変更できますが、Rustのletはプリミティブ型もヒープ型も一律で再代入を禁止します。
まとめ
Rustの変数は「不変がデフォルト」という原則を軸に、mut・シャドーイング・const・staticという4つの仕組みを組み合わせて使います。
let: 基本の変数宣言。不変がデフォルトで安全性が高いlet mut: 再代入が必要な場面で使用。カウンタやバッファに適している- シャドーイング: 型変換や段階的な値加工に適している。
mutより意図が明確になる場面がある const: コンパイル時定数。設定値や数学的定数に使用static: プログラム全体で生存する変数。可変にする場合はMutexやアトミック型を推奨
所有権・借用・ライフタイムはRustの変数の振る舞いと密接に結びついています。変数の基本を押さえたうえでこれらの概念を学ぶと、Rustのメモリ管理の設計思想が見えてきます。