🐙

Rust入門 ~その3 所有権を理解する~

2020/10/04に公開

前回までの記事

Rust入門 ~その1 環境構築からクイックスタート~
Rust入門 ~その2 Programming a Guessing Gameを理解する~

初めに

Programming a Guessing Gameを実際に動かしながら理解していきました。
今回は、そのままドキュメントを読み進めて所有権についての私の理解を書き残していきたいと思います。

メモリ管理

Rustにおける所有権とはメモリ管理の方法です。
Rust以外の言語でのメモリ管理の仕組みを記載したうえでRustにおける仕組みを説明します。

スタックとヒープ

ソフトウェアを動作させる際、スタックとヒープという二種類のメモリを使用しています。

スタックはFILO[1]の仕組みで高速に扱うことができます。しかし、固定長のデータを扱うことしかできないため、ソフトウェア実行時にサイズが動的に決まる情報を格納することはできません。

動的にサイズが決まる情報はヒープに格納をします。
ヒープは柔軟に利用することができる反面、スタックと比べて低速です。
ヒープ内の領域を動的に確保する場合、メモリの解放を着実に行う必要があります。
解放しなくてはそのうち領域が枯渇してしまうためです。

コードによるメモリ管理

古くから使用されている言語ではコードによってヒープ上の領域確保と解放を行っていました。
CやC++でのmallocですね。

コードで管理するということはプログラマが適切にメモリを扱う責任を負うということです。
ミスをしない人間などいないので、メモリの不適切な扱いによるバグがよく発生していました。

例えば以下のようなものです。

  • メモリの解放を忘れてメモリリークする。
  • まだ使用しているのに解放してしまい、データに参照できなくなる。

学生時代によくCでSegmentation Faultを起こしていたのが懐かしいです。
(今の学生も授業でCやるんですかね?さすがにもうやらないですかね?)

GCによるメモリ管理

プログラマによるメモリ管理によるバグを削減しようと言語の実行環境によるメモリ管理が登場します。
その仕組みがGC[2]です。

GCではコーディングした処理とは別にメモリ管理のための処理を実行し、不要となった領域の解放を行う仕組みです。
GCによってメモリ安全な仕組みが提供されたことでメモリ管理の不備によるバグは削減された一方で、計算資源(CPUやメモリ)を多く消費するようになりました。
さらには、ソフトウェアの動作を完全に停止させてしまう事象も発生することもありました。[3]

これらの問題に対処するために、GCを採用している言語では日々GCアルゴリズムの改善が行われています。
(Javaを例にとるとParallel→CMS→G1→ZGCとGCだけでもとても歴史を感じますね)

Rustが編み出した方法

所有権

Rustでは、コーディングによる管理とGCによる管理両方の課題を解消すべく第三のメモリ管理方法を採用しています。
それが所有権によるメモリ管理です。

基本的な考え方はGCよりもコードによるメモリ管理に近いと感じました。
ただし、解放をコードで記述するとバグを生み出す原因になるため、解放の仕組みを自動化してくれています。

重要な考え方は、メモリ上の情報に対する所有権を単一の変数が持つというものです。
そして所有権を持つ変数のスコープが終了すると、所有されたメモリは解放されます。

なぜ所有権という概念が必要かというと、複数の変数が同じメモリを参照することができるためです。
変数ごとに毎回メモリを確保していれば所有権の概念は不要なのですが、そんなことをすると変数を作成するたびにコピー処理をしなくてはならないうえ、多くのメモリを消費するようになるためリソース効率が悪くなってしまいます。

所有権に関する基本的なルールは以下の通りです。

  • 単一の変数のみが所有権を持つ。
  • 所有権を持つ変数がスコープアウトするとメモリが解放される。
  • 変数をほかの変数に代入すると代入先の変数に所有権が移る。
  • 変数を関数の引数に使用すると関数の引数に所有権が移る。
  • 関数の返り値にすると、関数呼び出し元の代入先変数に所有権が移る。

最後の二つは変数をほかの変数に代入する場合と本質的は同じですね。

複数の変数が同じメモリを参照している場合に、所有権がどう働くかはThe Bookの例がとても分かりやすいです。(以下のコードはThe Bookのコード抜粋です)

fn main() {
    // 略
    
    let s2 = String::from("hello");     // s2がスコープに入る

    let s3 = takes_and_gives_back(s2);  // s2はtakes_and_gives_backにムーブされ
                                        // 戻り値もs3にムーブされる
} // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので何も起きない。

// takes_and_gives_backは、Stringを一つ受け取り、返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。

    a_string  // a_stringが返され、呼び出し元関数にムーブされる
}

上記は所有権の移動についての例です。

main関数において、takes_and_gives_back(s2)を実行するとs2の所有権はtakes_and_gives_back関数の引数a_stringに移ります。

その後、a_stringは返り値としてmainのs3に代入されるため、s3に所有権が移ります。
このとき、a_stringはスコープアウトしていますが、所有権をもっていないためメモリは解放されません。

mainが終了する際に所有権を持つs3がスコープアウトするため、ここでメモリが解放されます。

借用

所有権を持たない変数は参照することができなくなります。
上記の例ではlet s3 = takes_and_gives_back(s2); の後にs2にアクセスしようとするとコンパイルエラーが発生します。
コンパイルエラーというのがいいですね。実行しなくてもメモリ安全でない処理に気付くことができるので。

しかし、毎回所有権のやり取りをしなくてはならないという縛りでは冗長なコードになってしまいます。
例えばある関数で使用したい変数があるとき、関数は必ず引数を返り値に含めて所有権を返さなくてはなりません。

このような事態を回避する仕組みが用意されています。
それが借用です。
借用とは、所有権を移動することなく異なる変数でメモリを参照可能にする仕組みです。

借用には変更不可な参照を渡す場合と、変更可能な参照を渡す場合の2種類があります。

それぞれの例を以下に示します。(The Bookままです)

不変な参照の例

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

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

可変な参照の例

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

変数名に&を直接つけるか、`&mut'をつけるかの違いですね。

借用に関するルールは以下の通りです。

  • 不変な参照は複数の変数に与えることができる。
  • 可変な参照は一つの変数にのみ与えることができる。(値の変更によるバグを防ぐため)
  • 不変な参照か可変な参照はいずれかしか与えることができない。

感想

無勉強でRustに突入したらハマりそうだなと思った反面、非常に強力な仕組みだと感じました。
ドキュメントの記載がとても分かりやすかったので、ちゃんとドキュメントを読んでからコーディングすればそれほど難しいと感じることはないのではないかと思います。

むしろコンパイル時に危ない変数の使い方をしている際にエラーとしてはじいてくれるのでプログラマーにとって優しい仕組みなのではないかと思いました。

次回

構造体とEnumまでThe Bookを読み進めたのですが、構造体とEnumについてはあまりRust固有な内容であるように感じなかったので、もっと読み進めて特徴的だなと思った部分を記事にしたいと思います。

脚注
  1. First In Last Out(最後に入れたものから順に取り出す) ↩︎

  2. Garbage Collection ↩︎

  3. いわゆるStop The World ↩︎

Discussion