Rustの所有権、3回目でやっと腑に落ちた
rust を触り始めて、最初の二回は所有権で挫折した。 三回目でやっと腑に落ちたので、何が分かっていなかったのかを残しておく。
一回目と二回目で何を間違えていたか
最初の二回、私は所有権を「ルール」として覚えようとしていた。 値はひとつの所有者しか持てない、参照は借用、可変参照は同時にひとつ──こういう箇条書きを暗記していた。
暗記したルールは、コンパイルエラーが出た瞬間に役に立たない。 なぜそのルールがあるのか分かっていないから、エラーメッセージを「禁止された」としか読めない。 禁止の理由が見えないと、回避策もその場しのぎになる。
三回目は「メモリの持ち主は誰か」から考えた
三回目に変えたのは、所有権をルールではなく「後始末の責任」として読み直したことだった。
所有権 とは要するに、そのメモリを誰が解放するかという責任の所在だ。
所有者がスコープを抜けるとき、drop が走って後始末をする。
責任がひとつに定まっていれば、二重解放も解放忘れも起きない。
この視点に立つと、ムーブが急に自然なものに見えてくる。
fn main() {
let s = String::from("hello");
let t = s; // s が持っていた責任が t に移る(ムーブ)
// println!("{s}"); // ここで s を使うとコンパイルエラー
println!("{t}"); // t が唯一の所有者
} // スコープ末尾で t の drop が走り、ヒープが解放されるlet t = s; で起きているのは、文字列の中身のコピーではない。
String はスタック上に「ポインタ・長さ・容量」の三つ組を持っていて、ムーブはこの三つ組だけを移す。
ヒープ上の hello 本体はそのまま、責任のラベルだけが付け替わる。
ムーブとメモリ配置、そして アラインメント
なぜ三つ組だけ移せば済むのか。
それは String というスタック上の値が、固定サイズ・固定配置の構造体だからだ。
スタックに置かれる値はサイズが静的に決まり、各フィールドは アラインメント に従って整列されている。
ポインタ幅と usize が揃っているからこそ、三ワードを丸ごと別の場所へ写すだけでムーブが完結する。
所有権の移動が「軽い」のは、この素直なメモリ配置に支えられているわけだ。
逆に言えば、ヒープ本体を触らずに済むからムーブは速い。 コピーに見えてコピーしていない、という違和感の正体はここにあった。
ライフタイムは「借用がいつまで有効か」の話
所有権が腑に落ちると、ライフタイムも急に読めるようになった。 借用は所有権を奪わず、一時的に参照を貸すだけ。 ならば「貸している間に持ち主が消えたら困る」のは当たり前で、ライフタイム注釈はその「困らない範囲」をコンパイラに伝える道具にすぎない。
借用チェッカは敵ではなく、後始末の責任が破綻していないかを先に教えてくれる校正者だ。
三回目にしてようやく、エラーメッセージが「禁止」ではなく「指摘」に見えるようになった。 ルールを覚えるのをやめて、責任の流れを追うようにしたら、所有権はただの自然な帰結になった。