リソース獲得は初期化である(RAII)
Essential前提知識
リソース管理はかつて手動だった。メモリを割り当て、ファイルを開き、ロックを取得する——そして、あらゆる終了パスで解放、クローズ、リリースを覚えておく。1つのパスを見逃せばリークになり、同じパスを2度踏めば状態を壊す。RAIIは「クリーンアップを忘れずに」を構造的な保証に置き換える。リソースはそれを保持する値と同じだけ生きる。
パターン
RAII(Resource Acquisition Is Initialization、リソース獲得は初期化である)はリソースのライフタイムを値のライフタイムに結びつける。
- 値が作られるとき、リソースが獲得される。
- 値が破棄されるとき、リソースが解放される——自動的かつ無条件に。
「破棄される」とは値がスコープを抜けることを意味する。コンパイラはそのタイミングを正確に知っているので、クリーンアップはオプションではなく忘れることもできない。
メカニズムとしての Drop
Rustでは、RAIIは Drop トレイトを通じて機能する。値がスコープを抜けると、コンパイラは drop の呼び出しを挿入する——実行がブロックの末尾から落ちようと、早期リターンしようと、? で抜けようと同じだ。Drop はトレイトの詳細を扱う。ここで重要なのは、すべてのスコープ終了がクリーンアップポイントだということだ。
{
let guard = mutex.lock().unwrap(); // ロック獲得
// ... クリティカルセクション ...
} // guard がドロップされる → ロックが解放される、無条件に
Box、Vec、そして標準ライブラリ
Box<T> が最もシンプルな例だ。Box を作るとヒープメモリが割り当てられ、Box をドロップするとそれが解放される。
{
let b = Box::new([0u8; 1024]); // ヒープ割り当てが獲得される
// ... b を使う ...
} // b がドロップされる → ヒープ割り当てが解放される
Vec<T>、String、File、そして外部リソースをラップするほぼすべての標準ライブラリ型が同じパターンに従う。コンストラクタが獲得し、drop の実装が解放する。通常のRustコードでは free、close、unlock を直接呼ぶことはない——スコープがその作業をしてくれる。
スコープは正確かつ網羅的
早期リターンやエラー伝播はリークしない。
fn process(path: &str) -> std::io::Result<()> {
let file = std::fs::File::open(path)?; // ファイルディスクリプタが獲得される
// ... ファイルを読む ...
Ok(())
} // file はここでドロップされる。関数がどのように返ったかに関わらず
? 演算子はエラー時に早期リターンし、その時点でスコープにあるすべての値に drop を呼び出す。「巻き戻してクローズを忘れる」という落とし穴はない。
所有権ルールはRAIIから生まれる
RAIIはプログラムのあらゆる時点で「誰がこの値をドロップするか」に対する明確な答えを1つ必要とする。2つの変数が同じヒープ割り当てを所有できるなら、drop がメモリを2度解放することになり、重大なバグになる。どの変数も所有していなければ、drop は決して実行されず、リークになる。
次に学ぶ所有権ルールは、すべての値にちょうど1人の所有者がいることを強制するコンパイラのメカニズムであり、したがってちょうど1回の drop 呼び出しが保証される。所有権はRAIIを体系化し、コンパイル時に検証したものだ。