Rustの変数とデータ型を徹底解説!let宣言の基本ルールとイミュータブルの秘密
生徒
「Rustを勉強し始めたんですが、変数の書き方が他の言語と少し違う気がします。letって何ですか?」
先生
「Rustでは、変数を宣言する時にletキーワードを使います。最大の特徴は、標準では値が書き換えられない『不変(イミュータブル)』であることですね。」
生徒
「えっ、変数なのに中身を変えられないんですか?不便じゃないんでしょうか?」
先生
「実はそれが、プログラムのバグを減らすRustの強力な武器なんです。もちろん、明示的に指定すれば変更も可能ですよ。まずは基本的な宣言方法から詳しく見ていきましょう!」
1. Rustの変数宣言の基本!letキーワードの使い方
Rustというプログラミング言語において、データを一時的に保存するための箱を「変数」と呼びます。この変数を作成することを「宣言」と言い、Rustではletというキーワードを使用するのが基本の形です。文末には必ずセミコロン(;)を付けるのがルールです。
例えば、数値の10を変数xに代入したい場合は以下のように記述します。Rustは非常に強力な「型推論」という機能を持っており、代入される値から自動的にデータ型を判断してくれるため、初心者の方でも直感的に書き始めることができます。しかし、明示的に型を書きたい場合は、変数名の後ろにコロンを付けて型を指定することも可能です。
fn main() {
// 基本的な変数宣言(型推論によりi32型になる)
let x = 10;
// 型を明示的に指定した宣言
let y: i32 = 20;
println!("xの値は: {}", x);
println!("yの値は: {}", y);
}
xの値は: 10
yの値は: 20
このように、letを使うことでコンピュータのメモリ上に値を保持する場所を確保できます。Rustは型に厳しい言語ですが、型推論のおかげで記述量を減らしつつ、安全性を保つことができる設計になっています。システムプログラミング言語でありながら、スクリプト言語のような書き心地を提供しているのがRustの魅力の一つです。
2. 不変性と可変性!なぜRustの変数は初期状態で書き換えられないのか
Rustの最もユニークな点の一つは、変数がデフォルトで「不変(イミュータブル)」であることです。他の多くの言語では一度作った変数の中身を自由に書き換えることができますが、Rustでそれをやろうとするとコンパイルエラーになります。これは、意図しないデータの書き換えによるバグを未然に防ぐための強力なガードレールです。
もし変数の値を後から変更したい場合は、mut(ミュータブル)というキーワードを追加する必要があります。これにより、その変数は「書き換え可能」であるという意思表示をコンパイラに伝えることができます。開発者が「このデータは変わる可能性がある」「このデータは絶対に変わらない」ということを明確に区別することで、コードの可読性と安全性が飛躍的に向上します。
fn main() {
// mutを付けることで可変(書き換え可能)になる
let mut counter = 1;
println!("初期値: {}", counter);
counter = counter + 1; // 値の更新
println!("更新後の値: {}", counter);
}
初期値: 1
更新後の値: 2
大規模な開発において、「どこで値が変わったのか分からない」という問題は非常によくある悩みです。Rustではmutが付いていない変数は絶対に変わらないことが保証されるため、安心してコードを読み進めることができるのです。この「不変性」をデフォルトにする設計思想は、近年のモダンなプログラミングにおいて非常に高く評価されています。
3. 変数と定数の違い!constの使い方とルール
Rustにはletによる変数宣言のほかに、constを使った「定数」の宣言があります。どちらも値を保持するものですが、明確な違いがいくつか存在します。まず、定数は常に不変であり、mutを付けて可変にすることは絶対にできません。また、定数はプログラムの実行中ずっと有効な値を定義するために使われます。
定数を宣言する際のルールとして、必ずデータ型を明示しなければならないという点があります。変数と違って型推論は効きません。また、慣習として定数名は「全て大文字」で記述し、単語の間はアンダースコアで繋ぐのが一般的です。定数はコンパイル時に値が確定している必要があるため、計算式などは定数のみで構成されている必要があります。
// 定数は関数の外(グローバルスコープ)でも宣言可能
const MAX_POINTS: u32 = 100_000;
fn main() {
println!("最大ポイントは: {}", MAX_POINTS);
// エラー:定数は再代入できません
// MAX_POINTS = 200_000;
}
定数はマジックナンバー(意味の分からない数字)を排除し、プログラム全体で共有したい設定値などを管理するのに非常に役立ちます。例えば、物理演算の定数や最大接続数など、プログラムの寿命全体を通して変わることのない値を定義する際に積極的に活用しましょう。
4. シャドーイング!同じ名前の変数を再定義する便利な仕組み
Rustには「シャドーイング」という面白い機能があります。これは、同じスコープ内で既に存在する変数と同じ名前の新しい変数を、再度letを使って宣言することです。これにより、前の変数は「隠され(シャドウ)」、新しい変数が有効になります。
シャドーイングの利点は、データの加工を行いたい時にわざわざlet mutを使わなくて済むことや、型を途中で変換できることです。例えば、ユーザーからの入力を文字列として受け取り、その後に数値として扱い直したい場合、別の変数名(input_str, input_numなど)を考える必要がなく、同じ名前で上書きするように記述できます。
fn main() {
let spaces = " "; // 文字列型のスペース
// シャドーイング:同じ名前で数値を代入(型が変わってもOK)
let spaces = spaces.len();
println!("スペースの数は: {}", spaces);
}
スペースの数は: 3
この機能は、データの変換プロセスを記述する際に非常にコードをスッキリさせてくれます。ただし、多用しすぎると「今どの変数を指しているのか」が分かりにくくなることもあるため、文脈に合わせて適切に使用することが重要です。シャドーイングは新しい変数を生成しているため、元の変数の不変性を保ったまま新しい状態を作れるというメリットもあります。
5. Rustの基本データ型:数値、論理値、文字型を学ぶ
Rustの変数には様々なデータ型があります。大きく分けると、単一の値を表す「スカラー型」と、複数の値をまとめる「複合型」に分類されます。スカラー型には、整数、浮動小数点数、論理値、文字の4つがあります。Rustはメモリ効率を重視するため、数値型だけでも非常に多くのバリエーションが存在します。
整数型には符号あり(i)と符号なし(u)があり、ビット数(8, 16, 32, 64, 128)によって範囲が異なります。例えば、i32は32ビットの符号あり整数です。浮動小数点数は、標準的なf64(64ビット)と軽量なf32があります。論理値(bool)はtrueかfalseのいずれかを取り、条件分岐などに使われます。文字型(char)は4バイトのUnicodeスカラ値を表し、日本語のような多言語も一文字として扱えます。
fn main() {
let is_rust_fun: bool = true; // 論理値
let heart_emoji: char = '❤'; // 文字型(Unicode)
let float_num: f64 = 3.1415; // 浮動小数点数
if is_rust_fun {
println!("Rustは楽しい! {}", heart_emoji);
}
println!("円周率の近似値: {}", float_num);
}
これらの基本型を理解することは、Rustの厳格な型システムと仲良くなるための第一歩です。最初はどの型を使えばいいか迷うかもしれませんが、整数のデフォルトはi32、浮動小数点数のデフォルトはf64と覚えておけば、大抵のケースで問題なく動作します。適切な型を選ぶことで、プログラムのメモリ使用量を最適化し、意図しない数値のオーバーフローなどを防ぐことができます。
6. 複合型で複数のデータを扱う!タプルと配列の基礎
単一の値だけでなく、複数の値をグループ化して扱いたい場合には「複合型」を使用します。Rustの主要な複合型には「タプル(Tuple)」と「配列(Array)」があります。タプルは、異なる型の値を一つにまとめることができる便利な型です。例えば、名前(文字列)と年齢(数値)を一つのセットとして扱いたい時に役立ちます。タプルは固定長であり、一度宣言するとサイズを変更することはできません。
一方、配列は「同じ型」のデータを並べて管理するための型です。配列もタプルと同様に固定長で、作成時に決めた要素数を後から増減させることはできません。もし、実行時にサイズが変化するリストを扱いたい場合は、別の「ベクタ(Vector)」という型を使用しますが、まずは基本である配列の概念をしっかり押さえましょう。配列はスタックメモリにデータが確保されるため、非常に高速に動作するという利点があります。
fn main() {
// タプルの宣言と分解
let person: (&str, i32) = ("太郎", 25);
let (name, age) = person;
println!("{}さんは{}歳です。", name, age);
// 配列の宣言(同じ型のみ)
let months = ["1月", "2月", "3月"];
println!("最初の月は: {}", months[0]);
}
タプルは関数の戻り値として複数の値を返したい時によく使われます。配列は、一週間の日数や月の名前のように、データの個数が決まっている場合に最適です。これらの複合型を使いこなすことで、関連するデータを論理的にまとめ、プログラムの構造をより分かりやすく整理することができます。データのまとまりをどう定義するかは、良い設計の基礎となります。
7. Rustの型変換!asキーワードを使った安全なキャスト
プログラミングをしていると、異なる型同士で計算をさせたい場面が出てきます。例えば、整数型と浮動小数点数を足し合わせたい場合などです。しかし、Rustは型に対して非常に厳格なため、異なる型をそのまま計算させようとするとコンパイルエラーになります。これを解決するために「型変換(キャスト)」が必要になります。
Rustで型変換を行うにはasキーワードを使用します。これを使うことで、ある型を別の型に明示的に変換できます。ただし、大きな型から小さな型へ変換する場合(例えばi64からi8など)は、データが入りきらずに数値が切り捨てられる可能性があるため、注意が必要です。安全性を重視するRustですが、この明示的なキャストはプログラマの責任において行われます。
fn main() {
let integer_val: i32 = 100;
let float_val: f64 = 55.5;
// 型を揃えないと計算できない
let sum = integer_val as f64 + float_val;
println!("合計値は: {}", sum);
}
このように、意図的に型を変えることで複雑な計算もスムーズに行えるようになります。Rustが自動で型変換(暗黙的な型変換)を行わないのは、開発者が気づかないうちに精度が落ちたり、予期せぬ動作をしたりすることを防ぐためです。全ての型変換を明示的に書くことで、コードの意図が明確になり、後から見直した際にも「ここで型を変えているんだな」と一目で理解できるようになります。
8. スコープと変数の寿命!メモリ管理の裏側を知る
変数がどこまで有効か、という範囲のことを「スコープ」と呼びます。Rustでは、変数は波括弧{ }で囲まれたブロック内がその有効範囲となります。このスコープの仕組みは、Rustの最大の特徴であるメモリ管理(所有権システム)と密接に関係しています。変数がスコープを外れると、Rustはその変数が保持していたメモリを自動的に解放します。
他の言語のようにガベージコレクタが定期的に掃除に来るのを待つのではなく、スコープが終わった瞬間に「もうこのデータは使わない」と判断して即座にメモリを返却するのです。これにより、高い実行パフォーマンスを維持しながら、メモリリークを防ぐことができます。初心者の方は、まず「波括弧が終われば、その中で作った変数は消える」というルールを意識してみてください。
fn main() {
let outer = "外側";
{
let inner = "内側";
println!("{}と{}の両方が見える", outer, inner);
} // innerはここで消滅(ドロップ)
println!("outerは見えます: {}", outer);
// println!("{}", inner); // これはエラーになる!
}
スコープを意識することで、データの寿命をコントロールできるようになります。不要になったデータは早めにスコープを抜けるように設計すれば、メモリを効率的に使うことができます。Rustはこのシンプルなルールを積み重ねることで、安全で高速なプログラムを実現しているのです。最初は少し難しく感じるかもしれませんが、この仕組みに慣れると、低レイヤのメモリ管理を恐れることなく開発を楽しめるようになります。