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