Rustのmoveとimmutabilityについてのルール

Rustを勉強しながら書いていて驚いた点のメモ。

immutable変数で持っていたオブジェクトをmutable変数へmoveできる

Rustではletで宣言した変数はimmutableとなるが、immutable変数で持っていたオブジェクトであってもlet mutで宣言したmutable変数にmoveさせることができる。

例えば、以下のコードはコンパイルが通る。

let foo = "foo".to_string();
let mut bar = foo;
bar.push_str("_bar");
println!("foo = {}", bar);

どうもRustのimmutabilityはオブジェクトではなくて変数に紐づくようだ。

というより、代入演算子=はあくまでオブジェクトの複製を作る、という考え方なのだろうか。複製なので、右辺と左辺のmutabilityは独立になる。ただ、Copy traitを持たない場合は"たまたま"move semanticsになってしまう、ということなのか。

また、Rustには"interior mutability pattern"というのがある。つまり、CellRefCellといった型を使うと、immutable変数を通じて中身を書き換えられる。だとするとimmutableとは何だったのかという気分にもなる。なんとなく、Rustにとってのmutabilityというのはmemory safetyを確保できれば(つまりborrow checkerをパスできれば)そんなに厳密に守らなくてもいい概念なのかもしれない。

immutable変数はmoveするけどconstはmoveしない

C言語でプログラミングする際には、自分は定数をconst変数として宣言・定義することがよくある(これは悪い作法かもしれないが)。

一方、Rustではletで宣言するimmutable変数とconstで宣言する定数には違いがある。immutable変数はmoveするけど、定数はmoveしない。

つまり、以下のコードはコンパイルが通るが、

#[derive(Debug)]
enum Foo {
    Bar,
    Buzz,
}

fn consume_foo(f: Foo) {
    println!("Foo: {:?}", f);
}

fn main() {
    const F: Foo = Foo::Bar;
    consume_foo(F);
    consume_foo(F);
}

以下は通らない。

fn main() {
    let f = Foo::Bar;
    consume_foo(f);
    consume_foo(f);
}

fは最初のconsume_fooでmoveして無効になっているので、2回目のconsume_fooに渡すことはできない。

structのフィールドのmove

structのあるフィールドを外にmoveさせると、そのフィールドだけではなく基本的にstruct全体が無効化する。

よって、以下のコードはコンパイルが通らない。

#[derive(Debug)]
struct Foo {
    bar: String,
    buzz: String,
}

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    println!("foo: {:?}", f);
}

barへの代入の時点でfが無効化しているのでfprintln!することができない。

一方、別のフィールドへのアクセスはできる。なので以下のコードはコンパイルが通る。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    println!("buzz: {:?}", f.buzz);
}

ただし、クロージャが絡むと以下のような一見問題なさそうなコードがエラーになる場合がある。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    let c = || { println!("buzz: {:?}", f.buzz); };
    c();
}

どうもクロージャを作るときにfを丸ごとborrowしようとするらしく、f.barのmoveによってfが無効化しているのでエラーになるようだ。

この場合は以下のように書き直すとコンパイルが通る。

fn main() {
    let f = Foo { bar: "BAR".to_string(), buzz: "BUZZ".to_string() };
    let bar = f.bar;
    println!("bar: {:?}", bar);
    let buzz = f.buzz;
    let c = || { println!("buzz: {:?}", buzz); };
    c();
}

配列要素のmove

structと異なり、配列やVecの要素を外にmoveさせることはそもそもできないらしい。以下のコードはエラーになる。

fn consume_string(s: String) {
    println!("S: {:?}", s);
}

fn main() {
    let ss: [String; 3] = ["zero".to_string(), "one".to_string(), "two".to_string()];
    consume_string(ss[0]);
}

なお、配列の中の型がCopy traitを実装している場合はmoveにならないので上記のような要素のアクセスは許される。