所有権とライフタイム
所有権は Rust が爆発的に有名になるきっかけとなった機能です。 所有権により、Rust は完全にメモリ安全かつ、ガーベジコレクションがないため効率的になります。 所有権の詳細に立ち入る前に、この機能がなぜ必要なのかを考えてみましょう。
ガーベジコレクション(GC)が常に最適なソリューションではないこと、 手動のメモリ管理の方が望ましいケースもあることには異論はないと思います。 もしそう思わないなら、別の言語に興味を持った方が良いですよ?
あなたが GC のことをどう思っていようとも、GC はコードを安全にするためにとてつもない恩恵をもたらしました。 オブジェクトが早すぎるタイミングで消えてしまう心配が全く必要ないんです。 (とはいえ、そのオブジェクトへのポインタをその時点まで保有しておくべきかどうかというのは別の問題ですが・・・) これは、C や C++ プログラムが対処しなければならない、広範囲に広がっている問題です。 GC の無い言語を使ったことのあるひとなら誰でも一度はやってしまった、この単純な間違いを見てみましょう。
fn as_str(data: &u32) -> &str {
// 文字列を生成する
let s = format!("{}", data);
// しまった! この関数内でしか存在しないオブジェクトへの
// 参照を返してしまった!
// ダングリングポインタだ! メモリ解放後の参照だ! うわーー!
// (このコードは Rust ではコンパイルエラーになります)
&s
}
これこそが、Rust の所有権システムが解決する問題なのです。
Rust は &s
が生存するスコープを理解し、&s
がそのスコープ外に逃げることを防ぎます。
しかし、この単純なケースは、C コンパイラですらうまいこと防ぐことができるでしょう。
コードが大きくなり、様々な関数にポインタが渡されるようになると、やっかいなことになります。
いずれ C コンパイラは、十分なエスケープ解析ができなくなり、コードが健全である証明に失敗し、屈服することになるのです。
結果的に、C コンパイラはあなたのプログラムが正しいと仮定して、それを受け入れることを強制されます。
これは Rust では決して起こりません。全てが健全であるとコンパイラに証明するのはプログラマの責任なのです。
もちろん、参照が参照先のスコープから逃げ出していないことを検証することよりも 所有権に関する Rust の話はもっともっと複雑です。 ポインタが常に有効であることを証明するのは、もっともっと複雑だからです。 例えばこのコードを見てみましょう。
let mut data = vec![1, 2, 3];
// 内部データの参照を取る
let x = &data[0];
// しまった! `push` によって `data` の格納先が再割り当てされてしまった。
// ダングリングポインタだ! メモリ解放後の参照だ! うわーー!
// (このコードは Rust ではコンパイルエラーになります)
data.push(4);
println!("{}", x);
単純なスコープ解析では、このバグは防げません。
data
のライフタイムは十分に長いからです。
問題は、その参照を保持している間に、参照先が変わってしまったことです。
Rust で参照を取ると、参照先とその所有者がフリーズされるのは、こういう理由なのです。