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>> のように使う。マルチスレッドではデッドロックに注意する。

スマートポインタを最後の手段と考える必要はない。複雑に絡み合った参照の生存期間を扱う代わりにスマートポインタを取り入れるのは、負けを認めることではない。スマートポインタを使ったほうが、簡潔で、保守しやすく、使いやすい設計になる場合もある。

キーワード