22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rustでmatch内部の所有権について考える(暗黙の借用 vs. ムーブ)

Last updated at Posted at 2016-04-16

Rust の match が取る「式」と「ブランチ」の間で、所有権がどう扱われているのか考えたことはあるだろうか? 私はなかった。普段、コンパイルエラーにならないので、なんとなく使っていた、というのが正直なところだ。

でも知らないと困ることもあるだろう。そこで、まずデフォルトが、「借用」と「ムーブ」のどちらになっているのかを確認し、さらに、デフォルトと違う動作をさせるにはどうやるのかを紹介しよう。

なお、この記事を書くにあたり、以下を参考にした。

また、Rust の所有権システムについて、基本的なことを理解していると想定する。所有権システムについて学ぶには、日本語翻訳版のオフィシャルガイドを読むのがいいだろう。

Rust のバージョン

今回紹介するコードは、Rust 1.9 ベータ版で動作確認済み。Rust 1.0 以降なら、どのバージョンでも問題なく動くはずだ。

/home/tatsuya% multirust run beta rustc --version --verbose
multirust: checking metadata version
multirust: got metadata version 2
rustc 1.9.0-beta.1 (37a2869af 2016-04-12)
binary: rustc
commit-hash: 37a2869afa7eb0813becaf0070874806030052df
commit-date: 2016-04-12
host: x86_64-unknown-freebsd
release: 1.9.0-beta.1

/home/tatsuya% freebsd-version 
10.3-RELEASE

安定版でなく、ベータ版を使用したのは、たいした理由ではない。FreeBSD 10.3-RELEASE 上で multirust を使用した時に、安定版のインストールができない不具合が起きているからだ。(FreeBSD では、数日前から multirust が使えるようになったばかり で、まだ、こうした不具合があるようだ)

借用なのか、ムーブなのか

例として、単方向の linked list を使用する。

#[derive(Debug)]
struct List {
    value: i32,
    next: Option<Box<List>>,
}

Box は値を heap に置くので、それを要素として持つこの List では Copy トレイトは実装できない。従って、Listlet で変数に束縛するときは、ムーブセマンティクスとなり、所有権が移動する。

では、List を使ってみよう。まず、2要素の適当なリストを返すメソッドを作っておく。

impl List {
    fn sample_list() -> Self {
        List{
            value: 1,
            next: Some(Box::new(List {
                value: 2,
                next: None,
            }))
        }
    }
}

2016年4月17日:この章は書き直し中です

match の head expression と branch の間の所有権の扱いについて、最初に記事を書いた時の理解が間違っていたようです。現在、正しい振る舞いを確認中です。

後半のミュータブルな暗黙の借用に対するテクニックは有効です。

&mut について、暗黙の再借用という振る舞いがあり、後半のテクニックはこれに関係しているようです。

// &mut がデフォルトで「ムーブ」のため、コンパイルエラーになるケース
fn ex1() {
    let mut i = 0;
    let mb1: &mut i32 = &mut i;
    {
        let mb2 = mb1;  // note: `mb1` moved here because it has type `&mut i32`,
                        // which is moved by default
        *mb2 = 1;
    }
    let mb3 = mb1;      // error: use of moved value: 'mb1' [E0382]
    *mb3 = 2;

    println!("i = {}", mb3);
}

// &mut の「暗黙の再借用」が起こり、コンパイルエラーにならないケース
fn ex2() {
    let mut i = 0;
    let mb1: &mut i32 = &mut i;
    {
        let mb2: &mut i32 = mb1;  // 暗黙の再借用
        *mb2 = 1;
    }
    let mb3 = mb1;
    *mb3 = 2;

    println!("i = {}", mb3);
}

暗黙の借用はイミュータブルなら問題ない

さて、2要素固定のプログラムだとつまらないので、どんな長さのリストでも扱えるようにしてみよう。まず、値の読み出しのみで、変更しないケース、つまり、イミュータブルな操作を実装してみよう。

to_vec() というメソッドを実装する。これは、List から、Vec<i32> 型に変換するメソッドだ。

impl List {
    fn to_vec(&self) -> Vec<i32> {
        let mut current = self;
        let mut result = Vec::new();
        loop {
            result.push(current.value);

            match current.next {
                None => break,
                Some(ref next_list) => current = next_list,
            }
        }
        result
    }
}

current にリストの先頭の要素を束縛し、result.push(current.value) で、値を Vec<i32> に追加している。match で、次の要素があるか調べ、あるなら、それを current に束縛して繰り返す。要素がなくなったら、break でループを抜け、result を返す。

使ってみよう。

fn main() {
    let mut list = List.sample_list()
    println!("{:?}", list.to_vec());

このケースでは、コンパイルエラーが起きず、以下のように、期待した結果が得られた。暗黙の借用でうまく動くわけだ。

[1, 2]

ミュータブルだと問題あり

次は動かないケースを見よう。add(n: i32) という、リストの各要素に n の値を足すメソッドを作ってみる。

impl List {
    fn add(&mut self, n: i32) {
        let mut current = self;
        loop {
            current.value += n;

            match current.next {
                None => break,
                Some(ref mut next_list) => current = next_list,
            }
        }
    }
}

fn main() {
    let mut list = List::sample_list();
    println!("{:?}", list.to_vec());
    list.add(5);
    println!("{:?}", list.to_vec());
}

先ほどと構成は同じだ。変わったのは、引数が &self から &mut self ことくらいだ。

しかし、これはコンパイルエラーになる。

src/main.rs:50:22: 50:39 error: cannot borrow `current.next.0` as mutable more than once at a time [E0499]
src/main.rs:50                 Some(ref mut next_list) => current = next_list,
                                    ^~~~~~~~~~~~~~~~~
src/main.rs:50:22: 50:39 help: run `rustc --explain E0499` to see a detailed explanation
src/main.rs:50:22: 50:39 note: previous borrow of `current.next.0` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `current.next.0` until the borrow ends
src/main.rs:50                 Some(ref mut next_list) => current = next_list,
                                    ^~~~~~~~~~~~~~~~~
src/main.rs:53:6: 53:6 note: previous borrow ends here
src/main.rs:43     fn add(&mut self, n: i32) {
               ...
src/main.rs:53     }
                   ^
src/main.rs:50:44: 50:63 error: cannot assign to `current` because it is borrowed [E0506]
src/main.rs:50                 Some(ref mut next_list) => current = next_list,
                                                          ^~~~~~~~~~~~~~~~~~~
src/main.rs:50:22: 50:39 note: borrow of `current` occurs here
src/main.rs:50                 Some(ref mut next_list) => current = next_list,
                                    ^~~~~~~~~~~~~~~~~
error: aborting due to 2 previous errors
error: Could not compile `reborrow_vs_move`.

元々 current には、&mut self、つまり、ミュータブルな参照が束縛されている。これが match で 再度 ミュータブルな借用をしてしまい、そこから取り出した値が next_list に束縛される。

ミュータブルな借用は、あるスコープ内では1つしか持てないので、Some(ref mut next_list) => current = next_list が成立せず、コンパイルエラーになるのだ。

強制的にムーブする

        loop {
            current.value += n;

            match current.next {
                None => break,
                Some(ref mut next_list) => current = next_list,
            }
        }

さて、ここをどうするかだが、まず安易な解決法として、match の所で currentclone()(複製)することが考えられる。

しかし、ループに注目すると、match の前で current に元々束縛していたリストの要素は、match の後では、もう current に束縛されておらず、不要であるとわかる。すぐ不要になるのだから、clone() してしまうのは効率が悪い。

不要になるなら、match で所有権を移してかまわない。つまり、再借用ではなく、ムーブさせればいいわけだ。

関数でムーブする

match で current の値をムーブさせたいわけだが、Rust には、そのための専用構文はない。なぜなら、既存の仕組みでできるからだ。関数を使う方法と、ブロックを使う方法の、2つの方法がある。

まず、関数を使う方法を紹介しよう。以下のような関数を定義する。

fn moving<T>(x: T) -> T { x }

そして、match をこのように書き換える。

            match moving(current).next {

これでムーブが実現できた。

[1, 2]
[6, 7]

一体何が起きているのか?

でも、このシンプルな関数で、一体何が起こっているのか? この関数は、任意の型の値を引数として受け取り、その値を返しているだけだ。これでどうしてムーブできるのか?

その理由は、引数の型にある。これは x: T であって、決して x: &T とか、 x:&mut T ではない。x: T ということは、この関数を呼び出した時に所有権が移動(ムーブ)する。そしてそのムーブ済みの値 x を返しているので、match moving(current).next で、ムーブ済みの戻り値にアクセスすることになり、狙い通りの動作となるわけだ。

ブロックでムーブする

もうひとつの方法は、ブロック { } を使うことだ。このように書き換える。

            match { current }.next {

これだけで、moving() 関数と同じくムーブが起こる。

実は、ブロックの中で変数にアクセスすると、それだけで、所有権がムーブするのだ。そして、current がそのブロックの最後の式の値、すなわち、ブロックからの戻り値になっているので、ムーブ済みの値の .next にアクセスできる。

この方法の欠点は、ここに { } を付けてる意図が、数ヶ月たって読み返したときに、自分でも忘れているかもしれないことだろう。

またもうひとつの欠点として、rustfmt で自動整形した時に、以下のようになってしまい、読みにくいこともある。

            match {
                current
            }.next {

まとめ

  • match では、暗黙の借用が行われる。ほとんどの場面では、それで問題ない
  • match の「式」のところに書いた変数に、「ブランチ」のところで再束縛し
    ようとすると問題が起こる
  • その時は、moving<T>(x: T) -> T { x } というような関数か、ブロック
    { } を使って、所有権をムーブすればいい
22
20
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?