TL;DR
- Rustacean は参照でデータを共有し、所有やコピーを避けがちである。
- しかし、まずは使いやすさと保守性を優先し、パフォーマンスが本当に重要な場合にだけ最適化しよう。
RcやArcで共有所有にし、必要ならRefCellやMutexと組み合わせる設計は、負けを認めることではない。
内容
データ構造とメモリ確保
参照を使うと、データをコピーせずに利用できる。他の言語のポインタと似ているが、生存期間の制約と借用ルールがあるので、安全に再利用できる。
一方で、参照を使うとコードが扱いにくくなる。特に、フィールドを参照で持つデータ構造では、インスタンスを引き回すときに生存期間パラメータが付いて回る。
#[derive(Clone, Debug)]
pub struct Tlv<'a> {
pub type_code: u8,
pub value: &'a [u8],
}この問題は、フィールドを所有すれば回避できることが多い。
#[derive(Clone, Debug)]
pub struct Tlv {
pub type_code: u8,
pub value: Vec<u8>,
}大きくて悪いコピーなんて怖くない
Rustacean がコピーを避けがちなのは、.to_vec() や .clone()、Box::new の呼び出しのように、コピーやメモリ確保がコード上で明示されるからである。しかし、明示されているからといって、取り除く理由にはならない。最適化で使いやすさが犠牲になるなら、まず使いやすさを優先する。コピーを減らすのは、ベンチマークで改善が確かめられた場合だけにする。
ただし、覚えておくことが 2 つある。1 つ目は、コピーが明示的なのは「一般には」という点である。Copy トレイトを実装した型は、ムーブセマンティクスではなくコピーセマンティクスになり、暗黙にコピーされる。そのため、Copy はビット単位のコピーが正しく高速な型にだけ実装する(項目10)。追加データを持たない enum がその例である。
2 つ目は、alloc を使える no_std のビルドターゲットを選べば、no_std の制約を避けつつ、使いやすく再利用しやすいコードを書けるという点である(項目33)。
参照とスマートポインタ
データ構造がフィールドを所有すると、値が変わったときに、すべてのコピーを同期して更新する必要がある。この問題は、スマートポインタで単一所有から共有所有に移れば回避できる(項目8)。
Rc や Arc は参照カウントで共有所有を実現する。さらに値を変更する必要があれば、内部可変性を持つ型と組み合わせる。シングルスレッドでは Rc<RefCell<T>>、マルチスレッドでは Arc<Mutex<T>> のように使う。マルチスレッドではデッドロックに注意する。
スマートポインタを最後の手段と考える必要はない。複雑に絡み合った参照の生存期間を扱う代わりにスマートポインタを取り入れるのは、負けを認めることではない。スマートポインタを使ったほうが、簡潔で、保守しやすく、使いやすい設計になる場合もある。