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"というのがある。つまり、Cell
やRefCell
といった型を使うと、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
が無効化しているのでf
をprintln!
することができない。
一方、別のフィールドへのアクセスはできる。なので以下のコードはコンパイルが通る。
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にならないので上記のような要素のアクセスは許される。