相互運用性
積極的に一般的なトレイトを型に実装している (C-COMMON-TRAITS)
Rustのトレイトシステムは 孤立 を許容しません。
大まかに言うと、全てのimpl
はトレイトのあるクレートか、実装対象の型があるクレートに置かれる必要があります。
従って、型を定義するクレートは積極的に、可能な限り全ての一般的なトレイトを実装すべきです。
次のような状況を考えてみてください。
std
クレートがDisplay
トレイトを宣言。url
クレートがUrl
型を宣言、Display
を実装せず。webapp
クレートがstd
とurl
をインポート。
この場合、url
がDisplay
を実装していない以上、webapp
にはどうすることもできません。
(newtypeパターンは有効な回避策ですが、不便です。)
最も重要な、一般的なトレイトをstd
から挙げます。
ある型にDefault
を実装し、引数を取らないnew
コンストラクタを追加するのは
一般的かつ開発者の予期するところであることに注意してください。
new
は慣用的なコンストラクタであり、利用者はそれが存在することを期待します。
従ってコンストラクタが引数を取らないのが自然であれば、
default
と機能的に同一であったとしてもコンストラクタが存在しているべきです。
変換に標準のトレイトFrom
, AsRef
, AsMut
を用いている (C-CONV-TRAITS)
以下の変換用トレイトは理にかなう限り実装されているべきです。
以下の変換用トレイトは実装されるべきではありません。
上記のトレイトはFrom
, TryFrom
を基にしたブランケット実装を持つので、
こちらを代わりに実装してください。
標準ライブラリでの例
From<u16>
がu32
に実装されています。幅の小さい整数は常に幅の大きい整数に変換可能であるためです。From<u32>
はu16
に実装されていません。数値が大きすぎると変換できないためです。TryFrom<u32>
がu16
に実装されており、u16
に収まらないほど数値が大きければエラーを返します。From<Ipv6Addr>
がIpAddr
に実装されています。この型はv4とv6の両方のIPアドレスを表すことができます。
コレクションがFromIterator
とExtend
を実装している (C-COLLECT)
FromIterator
とExtend
を実装すると、
そのコレクションは以下のイテレータメソッドと共に使うことができるようになります。
FromIterator
はイテレータが含んでいる値からコレクションを生成するものです。
Extend
は既存のコレクションにイテレータの含む値を追加します。
標準ライブラリでの例
Vec<T>
はFromIterator<T>
とExtend<T>
の双方を実装しています。
データ構造がSerdeのSerialize
とDeserialize
を実装している (C-SERDE)
データ構造として使われる型はSerialize
及びDeserialize
を実装しているべきです。
明らかにデータ構造である型とそうでない型の間にはグレーゾーンが存在します。
LinkedHashMap
やIpAddr
はデータ構造です。
LinkedHashMap
やIpAddr
をJSONファイルから読み取りたい、
あるいはIPCを用いて他のプロセスに送りたいというのは当然の要求でしょう。
一方でLittleEndian
はデータ構造ではりません。
これはbyteorder
の使うマーカ型で、 最適化によって消えるため実行時には存在しません。
これらは違いが明らかな例です。曖昧な例に遭遇した場合、必要ならば
IRCの#rustあるいは#serdeチャンネルで助言を求めることができます。
クレートが他の用途でSerdeに依存していなければ、 Cargoのfeatureを使ってSerdeへの依存をオプショナルにしたいと思われるかもしれません。 そうすることで、下流のライブラリがSerdeへの依存を必要とする場合のみSerdeがコンパイルされるようになります。
他のSerdeベースのライブラリとの一貫性を保つため、Cargoのfeatureの名前は単に"serde"
とすべきです。
その他、"serde_impls"
や"serde_serialization"
のような名前は使わないでください。
deriveを使わない場合、典型的な例は以下のようになります。
[dependencies]
serde = { version = "1.0", optional = true }
# #![allow(unused_variables)] #fn main() { #[cfg(feature = "serde")] extern crate serde; struct T { /* ... */ } #[cfg(feature = "serde")] impl Serialize for T { /* ... */ } #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for T { /* ... */ } #}
そして、deriveを使う場合は以下のようになります。
[dependencies]
serde = { version = "1.0", optional = true, features = ["derive"] }
# #![allow(unused_variables)] #fn main() { #[cfg(feature = "serde")] #[macro_use] extern crate serde; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] struct T { /* ... */ } #}
型が可能な限りSend
,Sync
である (C-SEND-SYNC)
Send
およびSync
はコンパイラが可能だと判断できれば自動的に実装されます。
生ポインタを操作する型の場合、Send
およびSync
の実装が
その型のスレッドセーフ特性を正確に表すように注意してください。
以下のようなテストを追加することで、
Send
やSync
の実装が意図せず外れるような退行バグの発生を防ぐことができます。
# #![allow(unused_variables)] #fn main() { #[test] fn test_send() { fn assert_send<T: Send>() {} assert_send::<MyStrangeType>(); } #[test] fn test_sync() { fn assert_sync<T: Sync>() {} assert_sync::<MyStrangeType>(); } #}
エラー型の意味が分かりやすく、行儀の良い実装となっている (C-GOOD-ERR)
エラー型とは、パブリックな関数から返されるResult<T, E>
における、任意のE
のことです。
エラー型は常にstd::error::Error
トレイトを実装しているべきです。
これは例えばerror-chain
のようなエラー処理ライブラリによって異なるエラー型を抽象化して扱うために使われており、
その型を別の型のエラーのcause()
として使うことができるようになります。
加えて、エラー型はSend
、Sync
トレイトを実装するべきです。
Send
でないエラー型はthread::spawn
によって作られたスレッドから返すことはできません。
そしてSync
でない型はArc
を用いて複数のスレッドから参照することかできません。
これらはマルチスレッディングアプリケーションにおける、基本的なエラー処理での一般的な要求です。
Send
とSync
は、Send + Sync + Error
を要求するstd::io::Error::new
を用いて
カスタムエラーをIOエラーに変換する際にも必要です。
この指針について、注意が必要なのはreqwest::Error::get_ref
のようなErrorトレイトオブジェクトを返す関数です。
普通はError + Send + Sync + 'static
が呼び出し側にとって最も有用です。
'static
を追加することにより、Error::downcast_ref
を使うことが可能になります。
例えエラーとして返せる有用な情報が無かったとしても、決して()
をエラー型として使わないでください。
()
はError
を実装していないため、error-chain
のようなエラー処理ライブラリと共に使うことができません。()
はDisplay
を実装していないため、利用者が自分でエラー時のメッセージを記述する必要があります。()
はDebug
を実装していますが、unwrap()
した際に役に立たないものです。- 下流のクレートが
?
演算子を使うためには、意味の不明瞭なFrom<()>
をエラー型に実装しければなりません。
代わりに、クレートや関数毎に意味の分かるエラー型を定義し、
適切なError
およびDisplay
の実装を追加してください。
返すべき情報がないならばユニット構造体型として定義することができます。
# #![allow(unused_variables)] #fn main() { use std::error::Error; use std::fmt::Display; // これの代わりに…… fn do_the_thing() -> Result<Wow, ()> // こうして下さい fn do_the_thing() -> Result<Wow, DoError> #[derive(Debug)] struct DoError; impl Display for DoError { /* ... */ } impl Error for DoError { /* ... */ } #}
Display
によって与えられるエラーメッセージは小文字とし、最後に約物は付けないで下さい。
そして普通は簡潔なものにしてください。
Error::description()
の返すメッセージはあまり気にする必要はありません。
description()
ではなく常にDisplay
を使ってエラーを表示すべきですから、
適当に"JSON error"
のようなもので十分です。
標準ライブラリでの例
ParseBoolError
は、文字列をboolとしてパースするのに失敗した際、返される型です。
エラーメッセージの例
- "unexpected end of file"
- "provided string was not `true` or `false`"
- "invalid IP address syntax"
- "second time provided was later than self"
- "invalid UTF-8 sequence of {} bytes from index {}"
- "environment variable was not valid unicode: {:?}"
バイナリ数値型がHex
, Octal
, Binary
によるフォーマットをサポートしている (C-NUM-FMT)
これらのトレイトはフォーマット指定子{:X}
、{:x}
、{:o}
、および{:b}
を使用した際の
表現を制御します。
|
や&
などビット単位での操作が行われる数値型にはこれらのトレイトを実装してください。
これは特にビットフラグ型において妥当です。
struct Nanoseconds(u64)
のような、量を表す数値型においては必要ないでしょう。
読み書きを行うジェネリックな関数がR: Read
とW: Write
を値渡しで受け取っている (C-RW-VALUE)
標準ライブラリは次の2つの実装を含んでいます。
# #![allow(unused_variables)] #fn main() { impl<'a, R: Read + ?Sized> Read for &'a mut R { /* ... */ } impl<'a, W: Write + ?Sized> Write for &'a mut W { /* ... */ } #}
従って、R: Read
あるいはW: Write
という境界を持つジェネリックなパラメータを値で受け取る関数は、
必要ならばミュータブルな参照を受け取ることも可能になっています。
そういった関数のドキュメントには、ミュータブルな参照を渡すこともできると簡単に書いてあるべきです。
新規のRustユーザが頻繁にこの点に引っ掛かるためです。
例えば、複数回に渡ってあるファイルからデータを読み出したいにも関わらず、
関数がreaderを値で受け取るという場合、対処方法の分からないユーザがいます。
この場合、上記の実装を利用してf
の代わりに&mut f
を代わりに渡せるというのが答えです。