所有権と借用によるメモリ管理(Rustモデル)
GCも手動freeも使わずにメモリ安全を勝ち取る仕組みが分かる。所有権・借用・ライフタイムがなぜコンパイル時にダングリングや競合を潰せるのか、その原理を線形型の理論から解き明かす。
- 1.所有権は「各値の解放責任を持つ変数を常に1つだけ」に固定する規則。スコープを抜けた所有者が値を破棄するため、二重解放もリークも構造的に起きない。
- 2.借用は所有権を移さず参照を貸す仕組みで、共有参照は複数同時可・可変参照は同時に1つだけ。この排他則がデータ競合をコンパイル時に排除する。
- 3.ライフタイムは参照が有効な区間で、借用が所有者より長生きしないことを検査する。理論的基盤はアフィン型(値を高々1回使う線形型の緩和)にある。
メモリ管理の第三の道
実行時のメモリ管理には長く2つの選択肢しかありませんでした。プログラマが明示的に確保と解放を書く手動管理(C/C++)と、実行時に到達不能な領域を回収するガベージコレクション(Java、Go)です。前者はダングリングポインタや二重解放という未定義動作の温床になり、後者は実行時オーバーヘッドと停止時間(GCポーズ)を抱えます。
所有権モデルはこの二者択一に第三の道を示しました。いつ解放するかをコンパイル時に決定可能にすることで、GCランタイムなしに、かつ未定義動作なしにメモリを管理します。鍵は「値の寿命を型システムで追跡し、規則違反をコンパイルエラーにする」という発想の転換です。
所有権:解放責任の一意化
所有権モデルの中核は、次の3つの規則に集約されます。
- 各値は、所有者(owner)と呼ばれる変数をちょうど1つだけ持つ。
- 同時に所有者になれる変数は1つだけ。
- 所有者がスコープを抜けるとき、値はドロップ(破棄)され、確保していた資源が解放される。
ここで決定的なのが「所有者は常に1つ」という不変条件です。これにより「この値を解放する責任は誰にあるか」が一意に定まり、二重解放(複数の所有者が同じ領域を解放する)も解放漏れ(誰も解放しない)も原理的に発生しません。スコープ終了時の自動ドロップは、C++のRAII(資源獲得は初期化である)と同じ思想を、言語レベルの強制規則にまで引き上げたものです。
{
let s = String::from("hello"); // s がヒープ上の文字列を所有
use_value(s);
} // s がスコープを抜け、ここで自動的に drop され解放される
ムーブ:所有権の移動
所有者が1つしかないなら、ある変数から別の変数へ値を渡すとどうなるのか。所有権モデルでは、代入や関数への引き渡しで所有権そのものが移動します。これをムーブ(move)と呼びます。
let a = String::from("data");
let b = a; // 所有権が a から b へムーブ
// println!("{}", a); // コンパイルエラー: a はもう使えない(moved out)
ムーブ後、元の変数 a は「使用済み」としてマークされ、アクセスするとコンパイルエラーになります。これがなぜ重要かというと、もし a と b が同じヒープ領域を指したまま両方とも有効だと、スコープ終了時に2回ドロップされてしまうからです。ムーブは「所有権は1つ」という不変条件を代入操作でも守るための仕掛けです。整数のようにコピーが安価で所有権の概念を持たない型は、ムーブの代わりに値の複製(コピー)が行われます。
ムーブは値のビット列をコピーする点ではシャローコピーと同じですが、決定的な違いは「コピー元を無効化する」ことです。古い所有者を使用不能にすることで、同じ資源を指す有効なハンドルが2つ存在する状態を型システムが禁じます。これにより、深いコピーをせずに所有権だけを安全に引き渡せます。
借用:所有権を移さずに貸す
すべての受け渡しでムーブが起きると、関数に値を渡すたびに所有権を手放すことになり不便です。そこで値を読み書きするあいだだけ参照を貸す仕組みが借用(borrow)です。借用には2種類あり、両者は排他的な規則で支配されます。
| 種類 | 記法 | 同時に持てる数 | 用途 |
|---|---|---|---|
| 共有参照(不変借用) | &T | 何個でも可 | 読み取り専用のアクセス |
| 可変参照(可変借用) | &mut T | ちょうど1つだけ | 書き込みを伴うアクセス |
中心となる規則は「ある値に対して、複数の共有参照を持てるか、ただ1つの可変参照を持てるか、そのどちらか一方だけ」というものです。読み手は何人いてもよいが、書き手がいる間は他の誰も(読み手すら)アクセスできない。これは並行処理におけるReaders-Writerロックの不変条件を、コンパイル時に静的に課したものに相当します。
let mut v = vec![1, 2, 3];
let r1 = &v; // 共有参照
let r2 = &v; // もう1つの共有参照(OK、読むだけ)
println!("{} {}", r1[0], r2[0]);
let m = &mut v; // 可変参照(この時点で共有参照がもう使われないなら OK)
m.push(4);
この排他則が直接もたらす帰結が、データ競合の静的排除です。データ競合は「2つ以上のスレッドが同じメモリに同時アクセスし、少なくとも1つが書き込みで、同期がない」ときに起きます。可変参照が常に唯一であるなら、書き込みと他のアクセスが同時に存在しえず、競合の前提条件そのものが成立しません。並行性モデルで問題になる競合を、ロックの規律に頼らず型で封じる点が画期的です。
ライフタイム:参照が所有者より長生きしない保証
借用で残る最後の危険は、貸した参照が、貸し主(所有者)の寿命を超えて生き残ることです。これはまさにダングリング参照そのものです。これを防ぐのがライフタイム(lifetime)で、各参照が有効である区間を表します。
借用チェッカは「すべての参照のライフタイムが、その参照先の所有者のライフタイムに含まれる(はみ出さない)」ことを検証します。はみ出せばコンパイルエラーです。
fn dangle() -> &String { // エラー: 返す参照の寿命が足りない
let s = String::from("x");
&s
} // s はここで drop される。返した参照は解放済みを指す → 拒否
s は関数を抜けるとドロップされるため、その参照を戻り値として返すと、呼び出し側は解放済み領域を指すことになります。借用チェッカはライフタイムの包含関係が破れていることを検出し、コンパイルを止めます。実行時チェックではなく型検査の段階で潰すため、オーバーヘッドはゼロです。
初期のRustは参照の寿命をスコープ(中括弧)の終端まで機械的に延ばしていたため、実際には使い終わった参照が邪魔をして借用が通らない場面がありました。現在はノンレキシカルライフタイム(NLL)により、参照の寿命を「最後に実際に使われた地点」まで縮められます。これにより、制御フローグラフ上での実際の使用範囲を解析し、安全なのに拒否される偽陽性を大きく減らしています。
線形型・アフィン型という理論的基盤
所有権モデルは経験則の寄せ集めではなく、型理論の部分構造論理(substructural logic)に正確な基盤を持ちます。通常の型システムは、ある型の値を自由に再利用できます。具体的には次の2つの構造規則を暗黙に認めています。
| 構造規則 | 意味 | 禁止すると |
|---|---|---|
| 弱化(weakening) | 値を使わず捨ててよい | 値を必ず使う必要が生じる |
| 縮約(contraction) | 値を複製して何度でも使える | 値を高々1回しか使えない |
線形型(linear type)はこの両方を禁じ、値をちょうど1回使うことを強制します。アフィン型(affine type)は弱化だけを許し縮約を禁じる、つまり値を高々1回(0回または1回)使えるという緩和版です。Rustの所有権は、まさにこのアフィン型に対応します。ムーブ後に元の変数が使えなくなるのは縮約の禁止(複製不可)であり、値を使わずにスコープを抜けてドロップできるのは弱化の許容(捨ててよい)に当たります。
集合の記法で言えば、ある資源の有効なハンドルの集合は、ムーブによって常に要素数1(または所有権放棄後の空集合)に保たれます。例えばハンドルの集合が {owner} から {} へ遷移しても、決して {a, b} のように複製されることはありません。借用は、この線形に流れる所有権の本線から、一時的かつ追跡可能な参照を派生させる操作と理解できます。
純粋な線形型は「値を必ず使い切る」ことを要求するため、エラーで途中脱出する場合などにすべての資源を明示的に消費する記述が必要になり、実用上の負担が大きくなります。Rustがアフィン型(高々1回)を採るのは、未使用の値をスコープ終端で自動ドロップする弱化を許すことで、この負担を解消するためです。所有権が「ちょうど1回」ではなく「高々1回」である点が、安全性と書きやすさを両立させる勘所です。
安全の境界と脱出口
所有権・借用・ライフタイムの3点セットは、ダングリング、二重解放、リーク、データ競合をコンパイル時に排除します。重要なのは、これらが実行時コストゼロで達成されることです。検査は型検査の一部であり、生成されるコードには監視のための追加命令が入りません。これが「ゼロコスト抽象化」と呼ばれる所以です。
ただし、借用チェッカが静的に安全だと証明できる範囲は、本当に安全なプログラム全体の部分集合にすぎません。双方向リンクリストのように複数の所有者が必要な構造は、純粋な所有権モデルでは表現できません。そうした場合は、実行時に参照数を数える参照カウント(共有所有権)や、安全性の証明責任をプログラマが負うunsafeブロックという脱出口を使います。unsafeは規則を消すのではなく、コンパイラが検証できない不変条件の保証をプログラマ側へ移譲する明示的な境界です。
まとめ
所有権モデルは、メモリの解放タイミングを型システムに刻み込むことで、GCも手動freeも使わずにメモリ安全を達成します。所有権は解放責任を一意化し、ムーブがその一意性を代入越しに守り、借用が共有参照と可変参照の排他則でデータ競合を断ち、ライフタイムが参照の寿命の包含関係を検証する——この連鎖はすべてコンパイル時に閉じています。理論的にはアフィン型(高々1回使用)として定式化でき、イミュータビリティや厳密なエラー処理と組み合わさることで、実行時の安全コストを設計時の検証コストへ前倒しする一貫した世界観を形づくっています。
プログラミング Article
所有権と借用によるメモリ管理(Rustモデル)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
所有権
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 6
導入後に効く点
借用は所有権を移さず参照を貸す仕組みで、共有参照は複数同時可・可変参照は同時に1つだけ。この排他則がデータ競合をコンパイル時に排除する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 6
判断チェックリスト
- 自社の用途が「所有権 / 借用」に近いか確認する。
- 強みである「所有権は「各値の解放責任を持つ変数を常に1つだけ」に固定する規則。スコープを抜けた所有者が値を破棄するため、二重解放もリークも構造的に起きない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。