🦀

100日後にRustをちょっと知ってる人になる: [Day 11]所有権の理解

2022/09/02に公開

Day 11 のテーマ

Day 10 で Rust の言語仕様の特徴と言われている 所有権について調べてみました。
正直昨日の時点では分かったような、分かってないようなという感じだったかなと思います。
そこで、今日は改めて頭を整理しながら、この所有権の理解をしたいと思います。

所有権とは

所有権とは、Rust が持っているメモリ管理の仕組みです。
Rust は変数を束縛する (bind) という概念が存在していて、次のようなルールがあります:

  1. 所有者と呼ばれる変数に対応する
  2. 所有者は常に単一
  3. 所有者がスコープから外れると値の破棄

つまり、単一の所有者(変数)が値をスコープで定められている間は束縛している、ということが言えます。この束縛から開放(メモリの開放)が行われるのが、スコープから外れる時ということです。

{
    les s = "Rust"  // スコープ開始
    fn do_something() {
        s
    }
}                   // スコープ終了(メモリ解放)

所有権によるメモリ管理のメリット

従来のプログラミング言語のメモリ管理は以下のような方法で行われています:

  • プログラムにより明示的にメモリ領域の確保と解放を行う
  • ガベージコレクションにより不要なメモリ領域を自動的に解放する

Rust の場合は、スコープから外れることによりメモリ解放が行われるというシンプルな動作をしています。GCのような動作を重たい処理、あるいは予期できない処理が動くということがありません。

また、スコープを意識したコード設計を行うようになるため、変数のスコープを小さくするようになり可読性の向上や保守性があがり、綺麗なコードにする効果があるのではないでしょうか。

所有権の動作

コードを見ながら所有権について整理してみます。

所有権 とスコープ

スコープと所有者の関係を振り返ります。

let x = 1;
{
    let y = 2;           // ネストされたスコープでのみ有効な変数 y
    println!("{}", y);
}                        // y のスコープ終了
println!("{} {}", x, y); // 変数 y はスコープ外のためコンパイルエラーになる

波括弧でネストされたスコープの中でのみ y は有効になっています。
そのためスコープが終了した波括弧の外側では y にアクセスすることができません。既に解放されているためコンパイルエラーになります。

所有権と所有権の移動

以下の例をみてください。

let x = String::from("Rust"); // x が所有者になる
let y = x;                    // y に x の所有権が移動する (x は所有権を失う)
println!("{}", x);            // x は解放されているためエラーになる

文字列型 (String) のようにヒープに確保されるデータは、そのものが格納されるのではなく、データのポインタ (データの位置) が格納されます。
したがって、上記の例の場合は x に格納されていたポインタが y に移動しています。そのため、x にアクセスしようとしても既に解放されてしまっているためエラーになってしまいます。

Rust のすごいところ は、このエラーの発生が実行時にわかるのではなく、コンパイル時にわかるということなのです。つまり、コンパイルを行う時点で、最低限の Null アクセスエラーのようなことは回避できるということですね。

プリミティブ型の場合

整数型 (i32) のようなプリミティブ型は、ヒープにデータを確保しません。スタックで確保しています。変数でこのデータを受け渡す場合は、参照を移動させるのではなく、値をコピーしています。

let x = 1;                // x で値を保持
let y = x;                // y に x の値をコピー
println!("{}, {}", x, y); // 出力結果: 1, 1

所有権を移動させずデータを参照

参照を使うことで、所有権を移動させずににアクセスできます。

let x = String::from("hello"); // x が所有者になる
let y = &x;                    // y に x の所有権を移動させず、x を参照する
println!("{}, {}", x, y);      // 出力結果: hello, hello

& キーワードを使うことで、変数を参照として使用することができます。

Day 11 のまとめ

今日は、所有権について改めて考えてみました。昨日は少し概念を掴みきれなかったところがあったのですが、理解が進んだと思います。
昨日の時点ではアロケートしたメモリの処理などを考える必要があるのかな、などと思っていました。そもそもそれを考えなくてよくする仕組みこそが、Rust の所有権システム、ということなんですね。
この Rust の所有権システムをまとめると以下のような内容になります。

  • 所有権とは Rust のメモリ管理の仕組み
  • 変数を割り当てると、その変数が所有者になる
  • ヒープを使用してデータの確保を行う型 (文字列型など) は、変数の値を別の変数に割り当てると新しい変数が所有者になる (所有権の移動)
  • スタックを使用してデータの確保を行う型 (整数型などのプリミティブ型) は、値がコピーされるため所有権の移動は発生しない
  • スコープを外れたときにメモリが解放される

この所有権という仕組みが分かってくると Rust が随分とシンプルで安全なメモリ管理の仕組みを備えているということが分かってきました。

GitHubで編集を提案

Discussion