相互運用性

積極的に一般的なトレイトを型に実装している (C-COMMON-TRAITS)

Rustのトレイトシステムは 孤立 を許容しません。 大まかに言うと、全てのimplはトレイトのあるクレートか、実装対象の型があるクレートに置かれる必要があります。 従って、型を定義するクレートは積極的に、可能な限り全ての一般的なトレイトを実装すべきです。

次のような状況を考えてみてください。

  • stdクレートがDisplayトレイトを宣言。
  • urlクレートがUrl型を宣言、Displayを実装せず。
  • webappクレートがstdurlをインポート。

この場合、urlDisplayを実装していない以上、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アドレスを表すことができます。

コレクションがFromIteratorExtendを実装している (C-COLLECT)

FromIteratorExtendを実装すると、 そのコレクションは以下のイテレータメソッドと共に使うことができるようになります。

FromIteratorはイテレータが含んでいる値からコレクションを生成するものです。 Extendは既存のコレクションにイテレータの含む値を追加します。

標準ライブラリでの例

  • Vec<T>FromIterator<T>Extend<T>の双方を実装しています。

データ構造がSerdeのSerializeDeserializeを実装している (C-SERDE)

データ構造として使われる型はSerialize及びDeserializeを実装しているべきです。

明らかにデータ構造である型とそうでない型の間にはグレーゾーンが存在します。 LinkedHashMapIpAddrはデータ構造です。 LinkedHashMapIpAddrを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の実装が その型のスレッドセーフ特性を正確に表すように注意してください。 以下のようなテストを追加することで、 SendSyncの実装が意図せず外れるような退行バグの発生を防ぐことができます。


# #![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()として使うことができるようになります。

加えて、エラー型はSendSyncトレイトを実装するべきです。 Sendでないエラー型はthread::spawnによって作られたスレッドから返すことはできません。 そしてSyncでない型はArcを用いて複数のスレッドから参照することかできません。 これらはマルチスレッディングアプリケーションにおける、基本的なエラー処理での一般的な要求です。

SendSyncは、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: ReadW: 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を代わりに渡せるというのが答えです。