電気ひつじ牧場

技術メモ

Rustにっき/5日目・所有権&参照

5日目です。所有権の続きと参照について勉強します。

移動タイミング

こんな時に移動が発生します。

  • 変数へ代入する。
  • 関数に対して値渡しで引数を渡す。
  • 関数から値をreturnする。

歯抜けのベクタ?

変数にStringベクタ中のある要素を代入したいとします。

let ary = vec!["a".to_string(), "bb".to_string()];
let t = ary[0];

Stringのベクタの先頭要素を取り出しました。これをコンパイルします。

11 |     let t = ary[0];
   |         ^ help: consider using `_t` instead

error[E0507]: cannot move out of borrowed content
  --> src/lib.rs:11:13
   |
11 |     let t = ary[0];
   |             ^^^^^^
   |             |
   |             cannot move out of borrowed content
   |             help: consider borrowing here: `&ary[0]`

エラーになりましたね。Stringのような型の要素をもつ配列やベクタは要素を変数に代入することができないようです。これは代入に伴い移動が起こるからです。代入に限らず、配列やベクタ要素の移動を起こすような挙動はコンパイル時に検出され拒絶されます。

例えばこういうのもダメです。(Rustはセミコロン無しの最終行をreturnする)

fn sample() -> String {
    let myAry = vec!["a".to_string()];
    myAry[0]
}

要素の移動が起こるとなぜダメなんでしょうか?

もし仮に要素が移動されてしまうと、その後のベクタはその要素だけ未初期化の状態になってしまい、その状態を管理する必要が生じてしまうからです。「//このベクタは2番目だけ移動させたからそこだけ未初期化状態で、0,2,3...のようにアクセスする」なんてコメントが書いてあったら僕なら会社を辞めます。

要するに、「歯抜けのベクタ」が生じないようになっているのです。

コピー型

移動について話してきましたが、実は全ての値が代入などに伴い移動される訳ではありません。intやcharなどは解放する必要のあるバッファも持たないため移動されずに値がコピーされます。所有権も新しい変数がコピーされた値を所有するので、唯一の所有者ルールが破られることもなければ、元の変数が未初期化状態になることもありません。

このような型をCopy型と言います。基本ルールとして値がドロップされる際に何か処理(解放やクローズ)をしなければならない型はCopy型ではありません。

独自に定義した型

ユーザー定義のstructはデフォルトでCopy型ではありませんが、全てのフィールドがCopy型である場合に限り構造体定義の上に#[derive(Copy, Clone)] と書くことでCopy型にすることができます。

所有権の共有

唯一の所有権ルールがRustの安全性の大きな柱だったと思うのですが、「共有される値は不変」という制約の元に安全に破ることができます。rc型という型を使います。

use std::rc::Rc;
let s = Rc::new("bar".to_string()); //型は推論される。Rc<String>型。t, uも同様
let t = s.clone();
let u = s.clone();

// t.push_str("hoge"); //コンパイルエラー。値は変更できない。
// let v = u;
// let w = u; // コンパイルエラー。移動した値を利用している。

上のコードによりs, t, uは"bar"というString型と付随する参照カウントに対するポインタになります。Rc型のポインタ自体は普通の所有権ルールに従い、(コードの下2行目参照)s, t, uが全てスコープから抜けるなどでドロップされると、Stringもドロップされます。

ここにきて参照カウンタかよ!!GCもないのに!!と突っ込みたくなる気持ちはわかりますが、Rustでは「共有される値は不変」という制約のおかげでオブジェクトを循環的に参照をするポインタは基本的に作成されません。つまり、お互いがお互いを参照していて、参照カウントが両者とも1となり、解放されないなんてことは起こりません。安全です。

参照

値を代入するたびに移動されていてはおちおち代入もできないので参照を使います。参照を作成することをRust用語で借用と言ったりします。

共有参照

readonlyの参照です。ある値に対して複数作成できます。let e: T の時&e&T型の参照を作成できます。共有参照自体はCopy型です。

let mut table = Table::new();
show(&table);

可変参照

参照先の値を変更できる参照です。なんだか怖そうですね。let mut e: T の時&mut e&mut T型の参照を作成できます。他の参照(共有参照含めて)と同時に作成することはできません。Copy型でもありません。

let mut table = Table::new();
show(&mut table);

複数読み出し単一書き込み

値が共有参照で借用されている間は、所有者であってもその値を変更することはできません。前節に出てきたrcの「共有される値は不変」ルールと同じですね。また可変参照で借用されているときは、その参照が独占的なアクセス権を持ちます。

共有参照、可変参照の例です。tableを定義します。

use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>;

fn main() {
    let mut table = Table::new();
    table.insert("a".to_string(), vec!["rrr".to_string(), "ccc".to_string()]);
    table.insert("b".to_string(), vec!["xxx".to_string(), "zzz".to_string()]);
    table.insert("c".to_string(), vec!["yyy".to_string(), "qqq".to_string()]);
}

テーブルの要素を表示する関数を書いてみます。

fn show(table: &Table) {
    for (key, values) in table {
        println!("key: {}", key);
        for val in values {
            println!(" {}", val);
        }
    }
}

&Tableとして参照渡しで引数を受け取っています。呼び出し側はこうです。

show(&table);

次にテーブルのvalueをソートする関数を作ります。

fn sort_value(table: &mut Table) {
    for (key, values) in table {
        values.sort();
    }
}

ソートは値を変更するため、&mut Tableとして可変参照を受け取っています。呼び出し側はこうなります。

sort_value(&mut table);

表示、ソート、表示の順で呼び出すとソートが確認できます。

show(&table);
sort_value(&mut table);
show(&table);

参照渡ししない限りこの呼び出しかたはできません。値渡しの場合1度目のshow(table)でtableが未初期化状態になってしまうからです。

続く

結構書いてしまった・・・続きは次回にします。