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
トレイトは実装できない。従って、List
を let
で変数に束縛するときは、ムーブセマンティクスとなり、所有権が移動する。
では、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 の所で current
を clone()
(複製)することが考えられる。
しかし、ループに注目すると、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 }
というような関数か、ブロック
{ }
を使って、所有権をムーブすればいい