【Rust】所有権システム、所有権の移動

1. 概要 

今回はRustのメモリ管理機能である、所有権システムについてやっていきます。

2. スタック領域・ヒープ領域

所有権システムについてやっていく前に、スタック領域とヒープ領域について簡単に説明します。

PCのメモリ領域は用途によってスタック領域とヒープ領域に大別されます。

使い分けとしては、スタックには固定長のデータを、ヒープには可変長のデータを格納すると言うのが基本になります。

スタックに格納されるデータはプログラムをコンパイルする時点であらかじめデータのサイズがわかっています。例えば、i32型の変数がプログラム上で定義されている場合、コンパイル時点でこのデータに必要なサイズは32ビットであるとわかります。理由はi32型の値は最大(最小)値を32ビットあれば表現できるためです。

一方で、ヒープに格納されるデータは、プログラムのコンパイル時点であらかじめデータのサイズが(厳密には)わかりません。例えば、String型にユーザが入力した文字列を受け取り、コンソール出力するというプログラムがあったとします。この場合、ユーザが入力する文字が何文字か、コンパイルの時点で知ることはできません。同様に、ユーザが入力した文字列を一文字ずつ分割して、これらをVec型(リスト)に格納するプログラムでは、コンパイル時点でリストの要素数が何個になるのか知ることはできません。
もちろん、String型やVec型の場合であっても、そこに格納される値がコンパイル時点で事実上確定しているケースもあります(例えばユーザの入力ではなくプログラム側で決まった値を代入する場合など)。しかし、この場合であっても、これらの型が可変長(例えば文字列の後ろに追加の文字列を結合する、リストに要素を追加することが可能)であるという性質を担保するため、データはヒープ領域に格納されることになります。

所有権を特に意識すべき場面は、ヒープ領域にデータが格納されるデータ型を扱う場面です。

3. 所有権システム

所有権システムとは、簡潔に言うと確保したメモリを、使用が終わった時点で自動的に破棄する機能です。他の言語だとGC(ガベージコレクション)が似たような機能です。所有権は、確保したメモリの利用できる範囲を明確に限定することで効率的にメモリを管理しています。

所有権の三大原則

  • 原則1:値には「所有権」があり、変数は値の「所有者」になれる。
  • 原則2:所有権は移動するが、「所有者」は1つだけである。
  • 原則3:「所有者」が有効スコープから出ると、値は破棄される。

三大原則について、実際にプログラムで確認してみましょう。

fn main() {
    let mut vec = Vec::new();
    vec.push(String::from("a"));
    vec.push(String::from("b"));
    vec.push(String::from("c"));

    for str in vec {
        println!("{}", str);
    }
}

実行

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/ownership-rust`
a
b
c

このプログラムは、Vec型の変数vecを定義した後に、pushメソッドを使ってString型の値を追加し、最後にfor文を使ってvecに格納されている要素を切り出して出力します。

それぞれの原則について、詳しく確認していきます。

原則1については、上記のプログラムであれば、変数vecがVec型の値の所有権を持つ所有者であると言うことです。この点については、そのままの意味なので深く考えなくても大丈夫です。

次のプログラムを見てください。先ほどのプログラムと同様にVec型の変数vec1を定義した後に、pushメソッドを使ってString型の値を追加します。その後、vec1を別の変数vec2に代入した後に代入元である変数vec1を使ってfor文で、各要素を出力します。

fn main() {
    let mut vec1 = Vec::new();
    vec1.push(String::from("a"));
    vec1.push(String::from("b"));
    vec1.push(String::from("c"));

    let vec2 = vec1;

    for str in vec1 {
        println!("{}", str);
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0382]: use of moved value: `vec1`
 --> src/main.rs:9:16
  |
2 |     let mut vec1 = Vec::new();
  |         -------- move occurs because `vec1` has type `Vec<String>`, which does not implement the `Copy` trait
...
7 |     let vec2 = vec1;
  |                ---- value moved here
8 | 
9 |     for str in vec1 {
  |                ^^^^ value used here after move

warning: `ownership-rust` (bin "ownership-rust") generated 1 warning
error: could not compile `ownership-rust` due to previous error; 1 warning emitted

コンパイルに失敗しました、なぜなのかをエラー内容を確認してみましょう。まず、7行目で「value moved here」となっています。これは値の所有権がvec1からvec2に移動していることを指しています。次に9行目では「value used here after move」所有権を移動した値(vec1)を使用していると書かれています。

Rustでは、String型やVec型などの可変長のヒープ上にデータを格納する型は、値の代入、関数への値の受渡し、関数からの戻り値の受け取りの際などに、所有権が移動します

所有権の移動がおきると、移動元の変数(vec1)は未初期化の状態になり、移動先の変数(vec2)がVec型の値の所有権を持ちます。そのため、上記プログラムでは、所有権が移動した未初期化の状態になった変数vec1を参照しようとしたことが原因でエラーになっています。これで所有権の所有者は1つ(1つの変数)だけという原則2が守られていることがわかります。

Rustでは{…}でスコープを表現できます。ブロックの外部からのアクセスはできないようになっています。また、if文やfor文で使用する{…}にも同様の性質があります。これを踏まえて次のプログラムを確認してみましょう。

fn main() {
    {
        let mut vec: Vec<String> = Vec::new();

        vec.push(String::from("a"));
        vec.push(String::from("b"));
        vec.push(String::from("c"));
    }

    for str in vec {
        println!("{}", str);
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0425]: cannot find value `vect` in this scope
  --> src/main.rs:10:16
   |
10 |     for str in vect {
   |                ^^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
error: could not compile `ownership-rust` due to previous error

エラーになりました。まずエラー内容ですが、vectがfor文を使用しているスコープでは見つからないと言われています。なぜかと言うと、vectはブロックの内部で定義している変数です。なのでブロックを抜けたタイミングで値がドロップされるからです。これが原則3の「所有者」が有効スコープから出ると、値は破棄されると言うことです。

CやC++ではfreeやdeleteを使用して明示的にメモリを解放しなければなりません。ここでよく起きるのがメモリの2重解放です。ですが、Rustはスコープを抜けた時に、自動的にdrop関数を呼び出して値を破棄します。また、Rustのコンパイラーにはメモリの2重解放を防ぐための機能があります。

次のプログラムを確認してみましょう。

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

        println!("{}", s2);
    }
}

先ほど、原則3でスコープを出ると、値は破棄されると説明しました。ですが、上記のプログラムでスコープ内の値、s1とs2の両方を破棄すると、2重解放が起きてしまいます。そのため、Rustのコンパイラーにはメモリが移動していることを検出し、移動先の値だけを破棄するようになっています。この、メモリの有効性を検証するコンパイラーの機能のことを、ボローチェッカー(Borrow Checker)と言います。

4. 所有権の移動

所有権の移動が発生するいろいろなパターンを確認していきましょう。

まず初めに、変数から変数へ所有権が移動するパターンです。

fn main() {
    // String型の所有権の移動
    let s1 = String::from("abc");
    let s2 = s1;

    println!("{}", s2);

    // Vec型の所有権の移動
    let vec1 = vec![String::from("a"), String::from("b"), String::from("c")];
    let vec2 = vec1;

    for str in vec2 {
        println!("{}", str);
    }
}

次に、関数への引数の受け渡しで所有権が移動するパターンです。

fn main() {
    // Vec<String>型のオブジェクト作成
    let vec: = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡す
    vev_print(vec);
}

/**
 * 受け取ったVec<String>を要素ごとに出力
 */
fn vev_print(vec: Vec<String>) {
    for str in vec {
        println!("{}", str);
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/ownership-rust`
a
b
c

問題なく実行できました。では次のようにした場合はどうでしょうか。

fn main() {
    // Vec<String>型のオブジェクト作成
    let vec = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡す
    vev_print(vec);

    // vecの要素ごとに出力
    for str in vec {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>を要素ごとに出力
 */
fn vev_print(vec: Vec<String>) {
    for str in vec {
        println!("{}", str);
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
error[E0382]: use of moved value: `vec`
 --> src/main.rs:9:16
  |
3 |     let vec = vec![String::from("a"), String::from("b"), String::from("c")];
  |         --- move occurs because `vec` has type `Vec<String>`, which does not implement the `Copy` trait
...
6 |     vev_print(vec);
  |               --- value moved here
...
9 |     for str in vec {
  |                ^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership-rust` due to previous error

エラーになりました。内容を確認してみましょう。所有権が移動済みの変数を使用したので怒られています。関数に引数として渡した場合も、所有権の移動は起こるので、エラーが発生しています。関数に渡した値を再度使用した場合は、関数の戻り値として受けります。そうすると関数vec_printに移動したvecの所有権を再度vecへと戻すことができます。

fn main() {
    // Vec<String>型のオブジェクト作成
    let mut vec = vec![String::from("a"), String::from("b"), String::from("c")];

    // 関数vec_printにvecを渡して、関数vec_printから戻り値を受け取る
    vec = vev_print(vec);

    // vecの要素ごとに出力
    for str in vec {
        println!("{}", str);
    }
}

/**
 * 受け取ったVec<String>をそのまま返す
 */
fn vev_print(vec: Vec<String>) -> Vec<String> {
    // 何かしらの処理
    vec
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/ownership-rust`
a
b
c

問題なく実行できました。上記プログラムのように、所有権そのものは不要だが、値に対してアクセスしたい場合に用いられるのが参照という機能です。所有権と同じくRustにおいて非常に重要な機能なので、次回あたりにいろいろやっていきたいと思います。

Rustの所有権のシステムは、すべてのデータ型が対象になるわけではなく、例外的なデータ型があります。それは「整数型」「不動小数点型」「論理値型」などのプリミティブ型で、スタック領域を使用する固定長のものです。では、実際に所有権が移動しないことを確認してみましょう。

fn main() {
    // 整数型
    let num1 = 30;
    let num2 = num1;

    println!("{}, {}", num1, num2);

    // 不動小数点型
    let f_num1 = 5.5;
    let f_num2 = f_num1;

    println!("{}, {}", f_num1, f_num2);

    // 論理値型
    let flg_a = true;
    let flg_b = flg_a;

    println!("{}, {}", flg_a, flg_b);
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/ownership-rust`
30, 30
5.5, 5.5
true, true

問題なく実行できました。このように所有権の移動ではなく、値のコピーが行われています。Rustのプリミティブ型にはコピートレイトが実装されています。トレイトは他の言語でいうところのインターフェースみたいなものです。なので、コピートレイトが実装されているプリミティブ型を他の変数に代入したり、関数に引数として渡したりしても、所有権の移動は行われず、値をコピーした新しいオブジェクト(所有権)を作成されるのです。

また、所有権が移動する型でも複製することで、所有権の移動を回避することもできます。

fn main() {
    // String型
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("{}, {}", s1, s2);

    // Vec型
    let vec1 = vec![String::from("a"), String::from("b")];
    let vec2 = vec1.clone();

    for str in vec1 {
        println!("{}", str);
    }

    for str in vec2 {
        println!("{}", str);
    }
}

実行します。

cargo run

結果

   Compiling ownership-rust v0.1.0 (<作業ディレクトリ>/ownership-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/ownership-rust`
hello, hello
a
b
a
b

問題なく実行できました。他にも所有権を共有したりなど所有権の移動を回避する方法はあります。これらについても後々、やっていきたいと思ってます。

5. まとめ

今回は、所有権についての解説をしました。他の言語の多くはメモリ管理をGC(ガベージコレクション)でしてます。なので、プログラマがメモリについて考えながら実装をすることはあまりないと思います。私自身、Rustを勉強するまで、スタックやヒープなどのことも考えることはほとんどありませんでした。所有権を勉強することは、メモリ周りの勉強にもなります。改めて、Rustは楽しい言語だと感じました。

投稿者プロフィール

OkawaRyouta
最新の投稿

関連記事

  1. 【Rust】文字列型

  2. 【Rust】構造体

  3. 【Rust】列挙型

  4. 【Rust】タプル型、配列型

  5. 【Rust】ベクタ、スライス

  6. 【Rust】文字型、論理値型

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. AWS
  5. AWS
  6. AWS
  7. AWS
  8. AWS

制作実績一覧

  1. Checkeys