項目10:標準トレイトに習熟しよう
一般的に使われている標準トレイト一覧
Clone: ユーザが定義したコードを実行して自身をコピーできる Copy: コンパイラ が(ユーザが定義したコードを実行するのではなく)メモリ上のデータをそのまま ビット 単位で複製する Eq: 2 つの値が比較できてその結果はいつも同じで、常に x == x(等価関係) これらのトレイトのうち、Display 以外は 自動導出(derive)が可能 Clone
このトレイトは、clone メソッドで値の新しいコピーが作れることを意味する
すべてのフィールドが Clone を実装してれば、derive 可能
導出された実装では、それぞれのメンバをクローンすることで、集約型 全体をコピーする Clone を実装すべきでない / できないケース
あるリソースへのユニークなアクセスを表しているケース
コピーを制限する理由があるケース
Clone でない構成要素があるケース
e.g.
フィールドが可変参照(&mut T)となっている場合
∵ 借用チェッカ は任意の時点で 1 つしか可変参照を認めない ため 上記 2 つのケースのいずれかに当てはまる場合
e.g.
Clone を手動で実装すべきケース
フィールド単位のコピーでは捉えられないアイテムがあるケース
e.g.
ファイルや ソケット のようなシステムリソースを扱う場合 キャッシュや メモ化 など、一時的な計算結果を保持する場合 Copy
code:rs
pub trait Copy: Clone { }
メモリ上のデータをビット単位で複製することを意味する
つまり、昔ながらのただのデータ型(POD: plain old data)であることを示すマーク Copy は Clone を実装しなければならないが、Copy 型を実装している型のインスタンスを コピーする際には、clone メソッドは用いられない
つまり、コンパイラはユーザ定義コードを用いずに新しい要素を生成する
ムーブセマンティクスの場合、代入演算子は右辺の 所有権 が左辺に移動(ムーブ)する したがって、下記のコードはコンパイルエラーとなる
code:rs
struct KeyId(u32);
let k = KeyId(42);
let k2 = k;
println!("k = {k:?}");
一方、コピーセマンティクスの場合は複製されるので、左辺も生存し続ける
code:rs
struct KeyId(u32);
ユーザ定義コードを実行する際には、明示的に clone メソッドを呼び出す
一方、暗黙的に実行される場合はユーザ定義コードを実行しない
Copy は Clone トレイト境界を持つので、常に clone メソッドを呼び出すことができるが、このメソッドを使うのは避けたほうが良い
∵ ビット単位のコピーのほうが、clone メソッドを呼ぶ出すよりも常に高速 である
code:rs
let k3 = k.clone();
warning: using clone on type KeyId which implements the Copy trait
実装すべきでない / できないケース
ビット単位のコピーで有効なアイテムが生成できないケース
このケースの多くは、Clone を derive するのではなく、手動で実装する必要がある
アイテムのサイズが大きいケース
∵ コピーが高速でないため
Clone でない構成要素があるケース
すべての構成要素が Copy であるケース
コンパイラはこのケースを指摘する Lint 項目(missing_copy_implementations)があるが、デフォルトではオフになっている
Default
すべての構成要素が Default を実装していれば、derive 可能
enum 型に対しても可能だが、この場合はどの バリアント をデフォルトとするか #[default] で指定する必要がある code:rs
enum IceCreamFlavor {
Chocolate,
Strawberry,
Vanilla,
}
code:rs
struct Color {
red: u8,
green: u8,
blue: u8,
alpha: u8,
}
let c = Color {
red: 128,
..Default::default()
};
PartialEq / Eq
ユーザ定義型の等価性を定義するために用いる
トレイトが存在すると、コンパイラは等価性チェック(==)の際に自動的にこれらのトレイトのメソッドを用いる
code:rs
pub trait Eq: PartialEq { }
つまり、Eq を実装しているすべての型 T は、型 T のすべての値 x: T が x == x を満たすことを保証する
x == x でないケースとは?
IEEE 754 では、NaN と何かを比較した場合には、NaN そのものとの比較であっても常に false を返す必要があることを定義している code:rs
assert_eq!(f64::NAN == f64::NAN, false);
x == x でない場合ならば、PartialEq を 実装したら Eq も実装すべき である
PartialEq を手動で実装すべきケース
e.g. 内部キャッシュなど性能向上のためのフィールドが含まれている場合
PartialOrd / Ord
ユーザ定義型の 2 つの値を比較するために用いる
トレイト境界に PartialOrd は PartialEq を、Ord は Eq を持つ
これらのトレイトは互いに整合する必要がある
トレイトが存在すると、コンパイラは比較チェック(<、>、<=、>=)の際に自動的にこれらのトレイトのメソッドを用いる
derive で自動生成されるデフォルト実装は、フィールド(バリアント)をコード上に定義された順番で比較する
{1, 2} は {1, 2, 3} の部分集合なので比較可能だが、{1, 3} は {2, 4} の部分集合ではないので比較不能
しかし、半順序が定義した型の挙動を正確に モデリング するものであったとしても、予期せぬ挙動になる場合があるので、PartialOrd だけを実装して Ord を実装しない場合には注意が必要 具体的には、if などでケースを見落とす可能性がある
code:rs
if x <= y {
println!("y is bigger");
} else {
println!("x is bigger");
}
すべてのケースを網羅するには、以下のように記述する必要がある
code:rs
if x <= y {
println!("y is bigger");
} else if y < x {
println!("x is bigger");
} else {
println!("Neither is bigger")
}
Hash
異なる値に対して(高確率で)一意となる値(ハッシュ値)を生成するトレイト ハッシュバケット に基づくデータ構造(HashMap や HashSet)のキーでは、このトレイトと Eq を実装する必要がある このとき、x == y ならば、常に hash(x) == hash(y) になるように実装する必要がある
Eq を独自に実装した場合、上記を満たすために Hash も独自実装する必要がないか確認する のが良い
Debug / Display
通常のフォーマット({})またはデバッグ出力({:?})で出力する際の出力形式を指定できる
これら 2 つのトレイトは、フォーマット指定子が異なるだけではない
Debug は derive できるが、Display はできない
Debug 出力のレイアウトは、Rust のバージョンが変わると変更される可能性がある
そのため、出力を別のコードでパースする必要がある場合などは、Display を使うのが良い
Debug は開発者向け、Display はユーザ向け
一般的に、定義した型に 機密情報が含まれていないならば、自動生成の Debug 実装を追加する のが良い
これを手助けするために、コンパイラには Debug を実装していない型を指定する、Lint 項目(missing_debug_implementations)が用意されている(デフォルトでは無効)
ここまでのまとめ
table:_
トレイト コンパイラでの使用 制約 メソッド
Clone clone
Copy let y = x; Clone マーカトレイト
PartialEq x == y eq
Eq x == y PartialEq マーカトレイト
PartialOrd x < y, x <= y, ... PartialEq partial_cmp
Ord x < y, x <= y, ... Eq + PartialEq cmp
Hash hash
Debug format!("{:?}", x) fmt
DIsplay format!("{}, x) fmt
他の項目で登場する標準トレイト
warning.icon いずれも、derive を用いた自動導出は不可能
Fn, FnOnce, FnMut: 実行可能な クロージャ を表す Error: 他のユーザやプログラマに対して表示可能なエラー情報を表す
ネストしたサブエラーに関する情報を含む場合もある
Drop: 破棄される際に何かを実行する
From, TryFrom: 他の型から自動的に生成できる
Iterator とそれに関するトレイト: 繰り返し実行の対象となる コレクション を表す Sync: 複数のスレッドから安全に参照できる
自動的にコンパイラによって実装される
table:_
トレイト コンパイラでの利用 制約 メソッド
Fn x(a) FnMut call
FnMut x(a) FnOnce call_mut
FnOnce x(a) call_once
Error Display + Debug [source]
From from
TryFrom try_from
Into into
TryInto try_into
AsRef as_ref
AsMut as_mut
Borrow borrow
BorrowMut Borrow borrow_mut
ToOwned to_owned
Deref *x, &x deref
DerefMut *x, &mut x Deref deref_mut
Index x[idx] index
IndexMut x [idx] = ... Index index_mut
Pointer format("{:p}", x) fmt
Iterator next
IntoIterator for y in x into_iter
FromIterator from_iter
ExactSizeIterator Iterator (size_hint)
DoubleEndedIterator Iterator next_back
Drop }(スコープの終わり) drop
Sized マーカトレイト
Send スレッド間で転送できる マーカトレイト
Sync スレッドをまたいで使用できる マーカトレイト
derive は不可
一般的に、演算子の意味を自然にできる 代数的 なオブジェクトを表す型でしか必要にならない 「関連性のない型に対して演算子をオーバーロードすべきではない」
コードの保守性が落ちる
予期しないパフォーマンス上の性質を持つ可能性がある
e.g. x * y で高価な O(N) のメソッドが実行される
また、実際に演算子をオーバーロードする際には、驚き最小の原則 に従って「整合性のために一連の演算子をすべてオーバーロードすべきだ」 e.g.
x + y と -y がオーバーロードされているなら、x - y もオーバーロードすべきである
演算子オーバーロードトレイトに渡された値は、所有権が移動する
この問題を解決するには、Copy トレイトを実装するか、&'a MyType に対する実装を追加すれば解決できる
table:_
トレイト コンパイラでの利用 制約 メソッド
Add x + y add
AddAssign x += y add_assign
BitAnd x & y bitand
BitAndAssign x &= y bitand_assign
BitOr x | y bitor
BitOrAssign x |= y bitor_assign
BitXor x ^ y bitxor
BitXorAssign x ^= y bitxor_assign
Div x / y div
DIvAssign x /= y div_assign
Mul x * y mul
MulAssign x *= y mul_assign
Neg -x neg
Not !x not
Rem x % y rem
RemAssign x %= y rem_assign
Shl x << y shl
ShlAssign x <<= y shl_assign
Shr x >> y shr
ShrAssign x >>= y shr_assign
Sub x - y sub
SubAssign x -= y sub_assign