【Rust】不用なCopyトレイトを実装しないこと

2020-06-14

配列状で管理している自作の構造体の値の変更が反映されないということがあった。 結果的には Copy トレイトを追加してしまっていたのが原因だった。

自作の構造体に対して、特に意識せずに #derive(Copy) でコピートレイトを指定:

#[derive (Clone, Copy, Debug)]
struct Foo {
x: i32,
}

していた。 その複数個を配列で、 Option での有無ありで管理:

let mut foos: [Option<Foo>; 3] = [
Some(Foo{x: 1}),
None,
Some(Foo{x: 3}),
];

していて、ループで更新処理を行っていた:

for p in foos.iter_mut() {
if let Some(foo) = p {
foo.x += 1; // なにか更新処理を行う
}
}

ここでちょっと色気を出して、 filter()None をはじいておいて unwrap() を使って if let Some(foo) = ... のインデントを減らそうかとしてみた:

for p in foos.iter_mut().filter(|x| x.is_some()) {
let mut foo = p.unwrap(); // -- (1)
foo.x += 1;
}

flat_map を使わないのは、更新した結果によっては None にクリアするケースがあったため。)

したら値が更新されなくなって結構悩んだ (Rustをよく理解してなくて雰囲気でいじってるのが問題なんだけど…)。 原因は (1) の unwrap() での Some 値の取り出しで Foo の値のコピーが発生していて、新しいインスタンスに対して変更してしまっていた。

Foo 構造体に Copy トレイトを指定していなければエラー:

error[E0507]: cannot move out of `*p` which is behind a mutable reference
--> src/main.rs:13:23
|
13 | let mut foo = p.unwrap();
| ^
| |
| move occurs because `*p` has type `std::option::Option<Foo>`, which does not implement the `Copy` trait
| help: consider borrowing the `Option`'s content: `p.as_ref()`

が出て、 as_ref() を使ってみろと教えてくれるので時間を無駄にせずに済んだ…。 実際には書き換えをしたいので as_mut() を使ってやる:

let mut foo = p.as_mut().unwrap();  // -- (1の修正)、 let foo = ...でもよい、がmutあってもワーニングは出ない

as_mut()&mut Option<Foo> から Option<&mut Foo> に変換できて、移動(またはコピー)じゃなく mut の借用になり、元の値の更新ができる。

結論

  • Copy トレイトを実装してしまうとついうっかりコピーが発生してしまう可能性があるので、 必要なければ追加しないこと(特に struct には)
  • ローカル変数は型宣言を省略できるので、よく理解してないと意図してない型を扱ってる可能性がある