Rust 基礎講座 #9 ライフタイムとモジュール — 注釈が必要になる瞬間、クレートで仕上げる
最終回です。残った約束は 1 つ、第 4 回で「参照がどれだけの間有効かを扱う検査」とだけ言っておいたライフタイム(lifetime)です。ライフタイムを整理した後、1 つのファイルを抜け出してプロジェクトを構成するモジュールと外部クレートまで扱い、講座を締めくくります。
ライフタイム — コンパイラはすでに計算していた #
第 4 回で、ローカル変数の参照を返すとコンパイルが拒否されると言いました。その判定の根拠がライフタイムです。コンパイラはすべての参照について「指している値が生きている区間」と「参照が使われる区間」を計算し、後者が前者をはみ出すと拒否します。これまで書いたすべての参照でこの計算はすでに動いていて、私たちが表記したことがなかっただけです。
表記が必要になる代表的な瞬間は、参照を複数受け取って参照を返す関数です。
fn longer(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}error[E0106]: missing lifetime specifier
|
1 | fn longer(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `a` or `b`エラーメッセージが理由を正確に語っています。返される参照が a 由来なのか b 由来なのか、シグネチャだけでは分からないということです。呼び出し側が戻り値をどれだけの間使ってよいか判定するには、その情報が必要です。答えはライフタイムパラメータです。
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}'a は初見では威圧的ですが、意味は一文です。「返される参照は、a と b のうち短く生きる側より長くは使えない」という関係の宣言です。 ライフタイム注釈は値を長生きさせる魔法ではなく、すでに存在する関係をコンパイラと呼び出し側に知らせるドキュメントです。この関係のおかげで、次のコードが正確に拒否されます。
let result;
{
let b = String::from("短命の文字列");
result = longer("長生きする側", &b);
} // b がここで解放される
println!("{result}"); // コンパイルエラー: b より長く使おうとした
幸い、実務でライフタイム表記を直接書く場面は思ったより少ないです。参照を 1 つだけ受け取って参照を返す関数のように関係が自明な場合は、コンパイラが省略規則(elision)で埋めてくれます。第 4 回の count_chars(text: &str) に表記がなかった理由です。構造体に参照を持たせるときもライフタイム表記が必要になりますが、基礎段階の実用指針は単純です。構造体のフィールドは参照(&str)より所有型(String)を既定値にし、ライフタイム表記が広がり始めたら設計を見直すシグナルとして読むことです。
モジュール — 1 つのファイルを抜け出す #
講座の間ずっと main.rs 1 ファイルでしたが、実際のプロジェクトはコードを分けます。Rust の単位はモジュールです。
// src/billing.rs
pub struct Invoice { pub amount: u32 }
pub fn issue(amount: u32) -> Invoice {
log_issue(amount);
Invoice { amount }
}
fn log_issue(amount: u32) { /* モジュール内部専用 */ }// src/main.rs
mod billing; // src/billing.rs をモジュールとして取り込む
use billing::issue;
fn main() {
let invoice = issue(50_000);
println!("発行金額: {}", invoice.amount);
}規則は 3 つで十分です。mod billing; がファイルをモジュールとしてつなぎ、すべての項目は既定で非公開なので外から使うものにだけ pub を付け、use でパスを縮めます。変数が不変既定だったように、可視性も非公開が既定です。公開 API が明示的な選択になるよう、言語が同じ方向に設計されています。
外部クレート — cargo add の 1 行 #
Rust のパッケージ単位はクレート(crate)と呼ばれ、公開レジストリは crates.io です。第 1 回で作った Cargo.toml の依存関係の管理も cargo が担当します。
cargo add randuse rand::Rng;
fn main() {
let dice = rand::rng().random_range(1..=6);
println!("サイコロ: {dice}");
}cargo add が Cargo.toml に依存関係を記録し、次のビルドでダウンロードしてコンパイルします。正確なバージョンは Cargo.lock に固定され、チーム全体が同じビルドを再現します。Python の requirements/lock、JavaScript の package.json/lock と同じ構図ですが、言語に最初から組み込まれていてツールが分裂していないのが、第 1 回で言った「cargo がすべて」の意味でした。どのクレートが標準的に使われているかは、シリアライズの serde、HTTP クライアントの reqwest、非同期ランタイムの tokio あたりの名前だけでも知っておくと、検索が楽になります。
講座を終えて — 次のステップ #
9 回の旅程を要約するとこうなります。Rust は所有権(第 3 回)と借用(第 4 回)で GC なしのメモリ安全を手に入れ、列挙型と Option・Result(第 5〜6 回)で「不在」と「失敗」を型で強制し、トレイト(第 8 回)で抽象化し、ライフタイム(今回)で参照の有効範囲を証明します。すべてが 1 つの原則、「ランタイムの事故をコンパイルエラーへ前倒しする」の変奏です。
次の学習経路としては 3 つをお勧めします。公式ドキュメントの The Rust Programming Language(通称 The Book)で、この講座が掘りきらなかった部分(クロージャの深掘り、スマートポインタ、並行処理)を埋めること。練習問題集の rustlings でコンパイラとの対話量を増やすこと。そのうえで、小さな CLI ツールを 1 つ自分で作り切ってみることです。Rust は、読んで分かったことと、借用チェッカーを通してみたことの差がとりわけ大きい言語です。
まとめ #
- ライフタイムは、すべての参照に対してコンパイラが常に計算している「有効区間」です。表記が必要なのは関係が曖昧になる箇所、代表的には参照を複数受け取って参照を返す関数だけです。
'aは寿命を延ばす装置ではなく、「返される参照は入力のうち短く生きる側を越えられない」という関係の宣言です。大半は省略規則が埋めてくれます。- 構造体のフィールドは所有型が既定値です。ライフタイム表記が広がったら、設計を見直すシグナルです。
- モジュールは
modでつなぎ、既定は非公開、公開するものにだけpubを付けます。外部クレートはcargo addの 1 行で、バージョンはCargo.lockが固定します。 - 次のステップは、The Book で空白を埋め、rustlings で練習量を積み、小さな CLI ツールを 1 つ完成させることです。基礎講座はここまでです。