Rustのcloneはディープコピーなのか
Rust LT会でclone
メソッドについての話があった。
考えると、そもそもRustでディープコピーかそうでないかを考えた経験がほとんどないので、改めてclone
の動作についてまとめてみる。
ディープコピー vs シャローコピー
JavaScript等でオブジェクトがネストするような形になっているオブジェクトを単純な複製すると、コピー元のネストしたフィールドの変更がコピー先に及ぶことがある。
let obj = { a: 0, b: 1, c: { d: 10 } };
let copy = { ...obj }; // またはObject.assign({}, obj)など
obj.a = 5;
obj.c.d = 100;
console.log(copy.a); // => 0
console.log(copy.c.d); // => 100
c
というフィールド内のd
というフィールドの変更がコピー先にまで反映されている。
これはこのc
というフィールドがオブジェクトへの参照という形で保持されていて、その参照をシンプルにコピーしてしまっているからである。
そのため、obj
とcopy
の持つc
フィールドは同じオブジェクトを参照しているため、このようにコピー後の変更まで反映されてしまう。
このようなコピーの仕方をshallow copy、つまり浅いコピーという。
参照先のオブジェクトの実体を複製してその複製への参照を持つことで、このような変更の反映を防ぐようなコピーはdeep copy、つまり深いコピーと言われる。
JavaScriptの場合、lodashのようなライブラリに付属している関数を利用したり、自前で何かしらの関数を用意するのが一般的な方法である。
Clone in Rust
Rustにおいてオブジェクトを複製したいときに使うのがclone
というメソッドになる。
Clone
トレイトを実装した型はclone
メソッドによってオブジェクトのコピーを取得できる。
通常、自分の定義した型にClone
を実装したい場合、#[derive]
を使う。これにより、各フィールドに対してclone
を呼ぶという実装が自動的にその型のclone
メソッドとなる。
ただし、すべてのフィールドがclone
を実装していないと、この方法は使えない。
#[derive(Clone)]
struct ChildStruct {
d: i64,
}
#[derive(Clone)]
struct MyStruct {
a: i64,
b: i64,
c: ChildStruct,
}
let mut obj = MyStruct { a: 0, b: 1, c: ChildStruct { d: 10 } };
let copy = obj.clone();
obj.a = 5;
obj.c.d = 100;
println!("{}", copy.a); // => 0
println!("{}", copy.c.d); // => 10
各フィールドについて、その値のclone
をとっているのでディープコピーのようにコピー先の変更に対する影響を受けない。
そもそも、各フィールドについて実体で持っていてJavaScriptのように参照を使っていないので、コードとして等価ではないとも言える。
では、無理やり参照を使いJavaScriptの方法と等価な方法を目指すとどうなるか、というと先ほどのLTのスライドにもあったように、そもそもコンパイルがうまくいかない。
#[derive(Clone)]
struct ChildStruct {
d: i64,
}
#[derive(Clone)]
struct MyStruct<'a> {
a: i64,
b: i64,
c: &'a ChildStruct,
}
let mut child = ChildStruct { d: 10 };
let mut obj = MyStruct { a: 0, b: 1, c: &child };
let copy = obj.clone();
obj.a = 5;
obj.c.d = 100; // cはミュータブルでない参照のため、コンパイルできない
child.d = 100; // objやcopyの中に自信の参照が残っているため、変更操作ができずコンパイルできない
println!("{}", copy.a);
println!("{}", copy.c.d);
このように、似たようなコードを素直につくろうとするとそもそもコンパイルができないというわけである。
では、少しひねくれた方法でこのようなコードを再現することを考えてみよう。
std::celll::Cell
を使えば内部可変性が得られるため、変更操作を隠蔽してコンパイルすることが可能である。
#[derive(Clone)]
struct ChildStruct {
d: Cell<i64>,
}
#[derive(Clone)]
struct MyStruct<'a> {
a: i64,
b: i64,
c: &'a ChildStruct,
}
let child = ChildStruct { d: Cell::new(10) };
let mut obj = MyStruct { a: 0, b: 1, c: &child };
let copy = obj.clone();
obj.a = 5;
obj.c.d.set(100);
println!("{}", copy.a); // => 0
println!("{}", copy.c.d.get()); // => 100
この方法なら確かにコピー先の変更を伝えることができる。
ただし、値をわざわざCell
で持っている時点で何かしらの意味があるとはわかるので、JavaScriptのように誤ってシャローコピーをして急に値が変わって驚く、みたいなことは少ないように思われる。
また、Cell
そのものclone
は内部の値を複製して新しいCell
をつくる実装になっているので、参照を経由してではなく直接Cell
を持つ場合はこのような問題は起こらない。
#[derive(Clone)]
struct ChildStruct {
d: Cell<i64>,
}
#[derive(Clone)]
struct MyStruct {
a: i64,
b: i64,
c: ChildStruct,
}
let child = ChildStruct { d: Cell::new(10) };
let mut obj = MyStruct { a: 0, b: 1, c: child };
let copy = obj.clone();
obj.a = 5;
obj.c.d.set(100);
println!("{}", copy.a); // => 0
println!("{}", copy.c.d.get()); // => 10
Cell
に限らず標準ライブラリに備わっているほとんどのジェネリックなデータ型は、内部の型のclone
を呼ぶ形でclone
を実装している(例: Box
, Vec
, std::collections::HashMap
)。
参照そのものclone
は参照先の値までは複製しないように、標準ライブラリの提供するジェネリックなデータ型でも参照やポインタとしての役割を持つデータ型は参照先の値をclone
しない。
代表的なのはstd::rc::Rc
とstd::sync::Arc
だろう。
"Rc"は参照カウント(Reference Count)を意味していて、参照カウンタ付きのスマートポインタで同じオブジェクトを共有したい場合に使う。内部可変性を持つためにstd::cell::RefCell
と使われることが多い。
Arc
はRc
スレッド安全版で、複数スレッド同じオブジェクトを共有したい場合に使う。内部可変性のためにstd::sync::Mutex
などのスレッド安全な型と合わせて使う。
Rc
を使う例は以下のようになる。
#[derive(Clone)]
struct ChildStruct {
d: i64,
}
#[derive(Clone)]
struct MyStruct {
a: i64,
b: i64,
c: Rc<RefCell<ChildStruct>>,
}
let child = ChildStruct { d: 10 };
let mut obj = MyStruct { a: 0, b: 1, c: Rc::new(RefCell::new(child)) };
let copy = obj.clone();
obj.a = 5;
obj.c.borrow_mut().d = 100;
println!("{}", copy.a); // => 0
println!("{}", copy.c.borrow().d); // => 100
Rc
で共有したフィールドなので、コピー元の変更が反映されてしまっている。ただし、Rc
やRefCell
を使っている時点で何かしらの意味があり使うメソッドもことなるので、やはり他の言語であるようなミスは少ないのでは、と考えられる。
まとめ
Rustのclone
は多くの場合ディープコピーのように振舞うが、これは各フィールドを参照ではなく実体として持つ場合が多いからである。
参照を使った場合や、Rc
のようなポインタとしての役割を持つ型を内部的に使うとシャローコピーのように振舞う。
しかし、型レベルでこれらの役割が明示されるため、フィールドを参照として持つ他の言語と比べると、ディープコピーを使うべきところでシャローコピーを使ってしまうというようなミスは比較的防ぎやすいように思う。
Discussion