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 == y | eq | |
Eq | 比較でき、常に x == x(同値関係) | x == y | PartialEq | なし |
PartialOrd | 一部の値どうしで順序付けできる(半順序) | x < y など | PartialEq | partial_cmp |
Ord | すべての値どうしで順序付けできる(全順序) | x < y など | Eq | cmp |
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 に対する実装を足す必要があり、定型的なコードが増える。