Rustの所有権と借用を理解する

Rust は所有権の機能を導入することでガベージコレクションを実装せずにメモリ安全を実現しています。これを理解しないとコンパイルエラーが発生した時になぜエラーなのかを把握するのか困難になるので、Rust を使う上で重要な概念です。

ドキュメントなどを読み概要が把握できたので、整理のために説明を書いていきます。

所有権の特徴

現在のプログラムにおける一般的なメモリの管理方法は大きく分けて二つあります。
一つは C や C++のようにプログラマが自分でメモリの確保と開放を宣言するやり方、二つ目は Java や Go のようにベージコレクションによって定期的に不要なメモリを開放する方法です。
一つ目の方法ではプログラマが常にメモリを意識して開発する必要があり非常にコストがかかり、メモリ管理を誤った場合にダングリングポインタやメモリ 2 重開放などでエラーが発生する危険性があります。また、二つ目の方法では定期的に実行されれるガベージコレクターがアプリケーションの動作の妨げになる事があります。

Rust では所有権システムに基づいてコンパイラがコンパイル時にメモリ安全性のチェックを行うことで、ガベージコレクションを組み込む事なく自動でメモリ安全性を保証しています。

所有権規則

所有権システムは次の規則を持っています。

  • 規則 1: 値は所有者と呼ばれる変数と対応している
  • 規則 2: 値を所有できる変数は必ず一つだけである
  • 規則 3: 値を所有する変数がスコープを抜けたら、値は破棄される

メモリと変数スコープ

fn main() {
    let s1 = String::from("hello");  // ここで
    println!("{}", s1);
} // ここでs1がスコープから抜けるので "hello" の値も破棄される

ここで登場している String 型 は可変の値なので、コンパイル時にサイズを決定する事ができないので、値はヒープ領域に配置されます。
このとき「値を所有する変数がスコープを抜けたら、値は破棄される」の規則に則り、s1 がスコープから抜ける時に、"hello" のメモリ解放がされます。

memory1

所有権のムーブ

次に、変数を別の変数に代入した場合のメモリの状態を考えてみます。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

この状態で先ほどと同じようにメモリの解放を考えてみます。最初に s2 がスコープから抜けるタイミングで、"hello" のメモリが解放されます。次に s1 がスコープから外れるので同じようにメモリを解放します。
ちょっと待ってください、ここで問題が発生しました!
"hello" のメモリは既に解放されているため、s1 でも同様にメモリ解放を実行すると、無効なメモリを解放する事になるのでエラーが発生します。

しかし、対象のコードはコンパイルに成功して正常に実行されます。
なぜでしょうか?

ここで最初に紹介した「値を所有できる変数は必ず一つだけである」事を思い出してみます。実は let s2 = s1; の部分で "hello" の所有権が s1 から s2 へ移動しており、s1 は値を所有しておらず変数として無効となっているのです。そのため、Rust は s1 がスコープから抜けるタイミングでメモリ解放をする必要がなくなり、結果として s2 に対してのみメモリ解放が実行されるため、正常に動作します。

この、値を所有する変数が移動することを 所有権のムーブ といいます。

move_ownership

この仕組みを考えると、例えば、次のコードはどうなるのでしょうか?

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
             -- value moved here
    println!("{}", s1);
                   ^^ value borrowed here after move
}

このコードはコンパイルエラーになります。"hello" の所有権が s1 から s2 にムーブしており、既に無効となっている s1 を使おうとしているためコンパイルエラーになるのです。

また所有権のムーブは関数に値を渡す場合も同様に発生します。

fn hello(s: String) {
    println!("hello! {}", s);
}

fn main() {
    let name = String::from("taro");
    hello(name);
          ---- value moved here
    println!("{}", name);
                   ^^^^ value borrowed here after move
}

借用

それでは、先ほどの例で関数に値を渡したい時はどうすればいいのでしょうか?
この問題は 借用 を使う事で解決できます。借用は & を使って表現します。

fn hello(s: &String) {
    println!("hello! {}", s);
}

fn main() {
    let name = String::from("taro");
    hello(&name);
    println!("{}", name);
}

借用は name への参照を作成し、これを所有しません。つまり、s は所有権を持たないのでスコープを抜けてもメモリ解放は実行されず、name がスコープを抜けたタイミングでメモリ解放が実行されます。 これは、コンパイラに対して「s は値を借りているだけなので、スコープを抜けてもメモリの解放をする必要がないよ」と教えているのです。

メモリ状態を表現すると次のようになります。

memory3

この借用を用いることで、所有権をムーブさせずに値を参照することができます。

参考

まとめ

Rust の所有権と借用の仕組みは理解するのに、かなり時間がかかりました。理解すると意外とシンプルな考えなんだなと感じました。
この記事は参考ドキュメントで理解した事を自分用に噛み砕いて書いただけなので、詳しい説明はドキュメントを読んだ方が良いです。

プロフィール
筆者のアバター画像
t-yng
フロントエンドエンジニア
タグ