TL;DR

  • よく使う標準トレイトの意味を知り、Display 以外は derive で自動導出できることを押さえる。
  • Copy はムーブではなくコピーで値を扱わせ、Eq や Ord は PartialEq や PartialOrd より強い性質を保証する。
  • 演算子のオーバーロードは、演算子の意味が自然に当てはまる型に限り、関連する演算子をまとめて実装する。

内容

https://effective-rust.com/std-traits.html

よく使う標準トレイト

トレイト意味コンパイラでの使用制約メソッド
Cloneユーザ定義のコードで自身を複製できるclone
Copyコンパイラがメモリ上のデータをビット単位で複製できるlet y = x;Cloneなし
Defaultデフォルト値のインスタンスを作れるdefault
PartialEq比較できるが、x == x とは限らない(部分同値関係)x == yeq
Eq比較でき、常に x == x(同値関係)x == yPartialEqなし
PartialOrd一部の値どうしで順序付けできる(半順序)x < y などPartialEqpartial_cmp
Ordすべての値どうしで順序付けできる(全順序)x < y などEqcmp
Hashハッシュ値を計算できるhash
Debugプログラマ向けに表示できる{:?}fmt
Displayユーザ向けに表示できる{}fmt

Clone

すべてのフィールドが Clone なら derive でき、各フィールドを複製して全体をコピーする。次の場合は Clone を実装すべきでないか、できない。

  • リソースへの一意なアクセスを表す(RAII、項目11)
  • 暗号鍵のように、コピーを制限したい
  • Clone でない要素を持つ。たとえば可変参照 &mut T のフィールドは、借用チェッカが同時に 1 つの可変参照しか認めないので複製できない。MutexGuard や Mutex も Clone ではない

逆に、Rc や Arc の参照カウント、ファイルやソケットなどのシステムリソース、キャッシュやメモ化のように、フィールド単位のコピーでは足りない場合は手動で実装する。

Copy

Copy はメソッドを持たないマーカトレイトで、ビット単位で複製できる昔ながらのデータ(POD)であることを示す。Copy な型を複製するとき、clone メソッドは使われない。

Copy は、代入の意味をムーブからコピーに変える。ムーブでは右辺の所有権が左辺に移るので、代入後に元の変数を使うとコンパイルエラーになる。Copy を付けると値が複製され、元の変数も使い続けられる。C++ のコピーコンストラクタと違い、Rust のコンパイラがユーザ定義のコードを暗黙に実行することはない。

Copy な型にも clone は呼べるが、ビット単位のコピーのほうが速いので避ける。Clippy も警告する。ビット単位のコピーでは有効な値にならない型や、サイズが大きくコピーが遅い型には Copy を実装しない。

Default

デフォルト値を作る default を提供する。enum にも derive できるが、デフォルトのバリアントを #[default] で指定する。構造体更新構文と組み合わせると便利である。ビルダパターンを使ってもよい(項目7)。

let c = Color {
    red: 128,
    ..Default::default()
};

PartialEq と Eq

== で使われる。Eq は PartialEq を拡張するマーカトレイトで、すべての値 x について x == x が成り立つことを保証する。成り立たない例は浮動小数点数で、IEEE 754 では NaN との比較は NaN 自身とでも常に偽になる。x == x が成り立つなら、PartialEq を実装したら Eq も実装する。キャッシュのように同一性に関係しないフィールドがあるときは、PartialEq を手動で実装する。

PartialOrd と Ord

< や >= などで使われる。derive した実装は、フィールドやバリアントを定義順に比較する。PartialOrd は、集合の包含関係のような現実の半順序を表せる。{1, 2} は {1, 2, 3} の部分集合だが、{1, 3} と {2, 4} は比較できない。ただし、PartialOrd だけを実装すると、次のような分岐で「どちらでもない」場合を見落としやすいので注意する。

if x <= y {
    println!("y is bigger");
} else if y < x {
    println!("x is bigger");
} else {
    println!("Neither is bigger");
}

Hash

HashMap や HashSet のキーには Hash と Eq が必要で、x == y なら必ず hash(x) == hash(y) でなければならない。Eq を手動で実装したら、Hash も合わせる必要がないかを確かめる。

Debug と Display

違いは書式指定子だけではない。Debug は derive できるが Display はできない。Debug の出力形式は Rust のバージョンで変わりうるので、出力を別のプログラムで解析するなら Display を使う。機密情報を含まない型には、derive(Debug) を付けておくとよい。

他の項目で扱う標準トレイト

次のトレイトは derive できない。

  • Fn、FnMut、FnOnce: 呼び出せるクロージャ(項目2)
  • Error: 表示可能なエラー情報(項目4)
  • Drop: 破棄されるときの処理(項目11)
  • From、TryFrom など: 型変換(項目5)
  • Deref、DerefMut: ポインタのように振る舞い、参照外しで中身にアクセスできる(項目8)
  • Iterator など: 繰り返しの対象(項目9)
  • Send、Sync: スレッド間で受け渡せる、共有できる。コンパイラが自動で実装する(項目17)

演算子オーバーロード

std::ops のトレイト(Add、Sub、Mul、Neg、AddAssign など)を実装すると、ユーザ定義型の単項演算子や二項演算子をオーバーロードできる。derive はできない。演算子の意味が自然に当てはまる代数的な型でしか必要にならず、関係のない型にオーバーロードすると保守性が落ちる。x * y で高価な O(N) の処理が走るような、予想外の性能になることもある。

また、驚き最小の原則に従い、関連する演算子はまとめてオーバーロードする。x + y と -y があるなら x - y も用意する。演算子のトレイトに渡した値は所有権が移るので、Copy を実装するか &'a MyType に対する実装を足す必要があり、定型的なコードが増える。

キーワード