項目10:標準トレイトに習熟しよう
https://effective-rust.com/std-traits.html
一般的に使われている標準トレイト一覧
Clone: ユーザが定義したコードを実行して自身をコピーできる
Copy: コンパイラ が(ユーザが定義したコードを実行するのではなく)メモリ上のデータをそのまま ビット 単位で複製する
Default: デフォルト値がセットされたインスタンスが生成できる
PartialEq: 2 つの値が比較できてその結果はいつも同じだが、常に x == x とは限らない(半等価関係)
Eq: 2 つの値が比較できてその結果はいつも同じで、常に x == x(等価関係)
PartialOrd: 一部比較可能で順序付けできる(半順序)
Ord: すべて比較可能で順序付けできる(全順序)
Hash: ハッシュ値を算出することができる
Debug: プログラマ向けに表示できる
Display: ユーザ向けに表示できる
これらのトレイトのうち、Display 以外は 自動導出(derive)が可能
Clone
このトレイトは、clone メソッドで値の新しいコピーが作れることを意味する
すべてのフィールドが Clone を実装してれば、derive 可能
導出された実装では、それぞれのメンバをクローンすることで、集約型 全体をコピーする
Clone を実装すべきでない / できないケース
あるリソースへのユニークなアクセスを表しているケース
e.g. RAII(項目11:RAIIパターンにはDropトレイトを実装しよう)
コピーを制限する理由があるケース
e.g. 暗号鍵 を保持している場合
Clone でない構成要素があるケース
e.g.
フィールドが可変参照(&mut T)となっている場合
∵ 借用チェッカ は任意の時点で 1 つしか可変参照を認めない ため
上記 2 つのケースのいずれかに当てはまる場合
e.g.
MutexGuard: ユニークなアクセスを内包
Mutex: スレッド安全のためにコピーを制限
Clone を手動で実装すべきケース
フィールド単位のコピーでは捉えられないアイテムがあるケース
生存期間 に関連した追加の管理が必要なケース
e.g.
Rc や Arc を使って 参照カウンタ を追跡する場合
ファイルや ソケット のようなシステムリソースを扱う場合
キャッシュや メモ化 など、一時的な計算結果を保持する場合
Copy
マーカトレイト の 1 つ
code:rs
pub trait Copy: Clone { }
メモリ上のデータをビット単位で複製することを意味する
つまり、昔ながらのただのデータ型(POD: plain old data)であることを示すマーク
Copy は Clone を実装しなければならないが、Copy 型を実装している型のインスタンスを コピーする際には、clone メソッドは用いられない
つまり、コンパイラはユーザ定義コードを用いずに新しい要素を生成する
Copy はコンパイラに対して、ムーブセマンティクス から コピーセマンティクス へ移行 するように指示する
ムーブセマンティクスの場合、代入演算子は右辺の 所有権 が左辺に移動(ムーブ)する
したがって、下記のコードはコンパイルエラーとなる
code:rs
#derive(Debug, Clone)
struct KeyId(u32);
let k = KeyId(42);
let k2 = k;
println!("k = {k:?}");
一方、コピーセマンティクスの場合は複製されるので、左辺も生存し続ける
code:rs
#derive(Debug, Clone, Copy)
struct KeyId(u32);
Rust は C++ の コピーコンストラクタ とは異なり、コンパイラがユーザ定義コードを暗黙的に実行することは無い
ユーザ定義コードを実行する際には、明示的に clone メソッドを呼び出す
一方、暗黙的に実行される場合はユーザ定義コードを実行しない
Copy は Clone トレイト境界を持つので、常に clone メソッドを呼び出すことができるが、このメソッドを使うのは避けたほうが良い
∵ ビット単位のコピーのほうが、clone メソッドを呼ぶ出すよりも常に高速 である
Clippy を用いると、警告を発する
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)があるが、デフォルトではオフになっている
https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#missing-copy-implementations
Default
デフォルトコンストラクタ(default)を与える
すべての構成要素が Default を実装していれば、derive 可能
enum 型に対しても可能だが、この場合はどの バリアント をデフォルトとするか #[default] で指定する必要がある
code:rs
#derive(Default)
enum IceCreamFlavor {
Chocolate,
Strawberry,
#default
Vanilla,
}
Default は 構造体の更新文法と組み合わせた場合に有効
code:rs
#derive(Default)
struct Color {
red: u8,
green: u8,
blue: u8,
alpha: u8,
}
let c = Color {
red: 128,
..Default::default()
};
もちろん、Builder パターン を用いても良い(項目7:複雑な型にはビルダを使おう )
PartialEq / Eq
ユーザ定義型の等価性を定義するために用いる
トレイトが存在すると、コンパイラは等価性チェック(==)の際に自動的にこれらのトレイトのメソッドを用いる
Eq は、PartialEq を拡張するただの マーカトレイト で、反射関係 を仮定して良いことを示す
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 で自動生成されるデフォルト実装は、フィールド(バリアント)をコード上に定義された順番で比較する
PartialOrd は (PartialEq とは異なり)、実世界の問題に対応している(半順序)
e.g. 集合の 部分集合 関係
{1, 2} は {1, 2, 3} の部分集合なので比較可能だが、{1, 3} は {2, 4} の部分集合ではないので比較不能
しかし、半順序が定義した型の挙動を正確に モデリング するものであったとしても、予期せぬ挙動になる場合があるので、PartialOrd だけを実装して Ord を実装しない場合には注意が必要
(項目2:型システムを用いて共通の挙動を表現しよう に反する稀有な例)
具体的には、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) になるように実装する必要がある
https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq
Eq を独自に実装した場合、上記を満たすために Hash も独自実装する必要がないか確認する のが良い
Debug / Display
通常のフォーマット({})またはデバッグ出力({:?})で出力する際の出力形式を指定できる
これら 2 つのトレイトは、フォーマット指定子が異なるだけではない
Debug は derive できるが、Display はできない
Debug 出力のレイアウトは、Rust のバージョンが変わると変更される可能性がある
そのため、出力を別のコードでパースする必要がある場合などは、Display を使うのが良い
Debug は開発者向け、Display はユーザ向け
一般的に、定義した型に 機密情報が含まれていないならば、自動生成の Debug 実装を追加する のが良い
これを手助けするために、コンパイラには Debug を実装していない型を指定する、Lint 項目(missing_debug_implementations)が用意されている(デフォルトでは無効)
https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#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: 実行可能な クロージャ を表す
項目2:型システムを用いて共通の挙動を表現しよう
Error: 他のユーザやプログラマに対して表示可能なエラー情報を表す
ネストしたサブエラーに関する情報を含む場合もある
項目4:標準のError型を使おう
Drop: 破棄される際に何かを実行する
RAII パターン の実装に必須(項目11:RAIIパターンにはDropトレイトを実装しよう)
From, TryFrom: 他の型から自動的に生成できる
項目5:型変換を理解しよう
Deref, DerefMut: ポインタオブジェクト として振る舞い、参照解決 すると内部に保持したアイテムへアクセスできる
項目8:参照型とポインタ型に慣れよう
Iterator とそれに関するトレイト: 繰り返し実行の対象となる コレクション を表す
項目9:明示的なループの代わりにイテレータ変換を使用することを検討しよう
Send: スレッド 間で安全に受け渡しできる
Sync: 複数のスレッドから安全に参照できる
自動的にコンパイラによって実装される
項目17:状態共有並列実行には気を付けよう
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 スレッドをまたいで使用できる マーカトレイト
演算子オーバーロード
std::ops モジュールの様々な標準トレイトを実装すると、ユーザ定義型に対する組み込みの 単項演算子 や 二項演算子 を オーバーロード できる
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
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目