Rust APIガイドライン (非公式日本語訳)
これはプログラミング言語RustのためのAPIデザイン上の推奨事項をまとめたものです。 大部分は、Rustエコシステム内の標準・非標準ライブラリを構築してきた経験を元に、 Rustのライブラリーチームによって執筆されました。
これはあくまで指針です。 項目ごとの充実度には差があり、今後の加筆修正が必要となっている曖昧な部分もあります。 Rustのクレート開発者は、ここから使えそうな内容を選び取り、 イディオマティックで相互運用性の高いライブラリを開発する上での重要な検討事項とするべきです。 また、このガイドラインは義務的に従わなければならない基準とされるべきではなく、 あくまで従わない場合に比べエコシステム内の他のクレートとのより良い相互運用が達成できるような 物となっています。
このブックはチェックリストと各項目の説明に分かれています。 チェックリストは簡潔に全項目をまとめたものです。 クレートのレビューを行う際に、ざっと目を通すのに使えます。 残りそれぞれの項目に関して章分けし、詳しい説明を行っています。
APIガイドラインへの貢献に興味がある場合、contributing.mdを参照し、 Gitterチャンネルに参加してください。
Rust API Guidelines Checklist
- 命名 (クレートがRustの慣用的な命名規則に従っている)
- 相互運用性 (クレートが他のクレートの機能とうまく連携できる)
- 積極的に一般的なトレイトを型に実装している (C-COMMON-TRAITS)
Copy,Clone,Eq,PartialEq,Ord,PartialOrd,Hash,Debug,Display,Default
- 変換に標準のトレイト
From,AsRef,AsMutを用いている (C-CONV-TRAITS) - コレクションが
FromIteratorとExtendを実装している (C-COLLECT) - データ構造がSerdeの
SerializeとDeserializeを実装している (C-SERDE) - 型が可能な限り
Send,Syncである (C-SEND-SYNC) - エラー型の意味が分かりやすく、行儀の良い実装となっている (C-GOOD-ERR)
- バイナリ数値型が
Hex,Octal,Binaryによるフォーマットをサポートしている (C-NUM-FMT) - 読み書きを行うジェネリックな関数が
R: ReadとW: Writeを値渡しで受け取っている (C-RW-VALUE)
- 積極的に一般的なトレイトを型に実装している (C-COMMON-TRAITS)
- マクロ (クレートが行儀のよいマクロを提供している)
- 入力の構文から結果をイメージできるようになっている (C-EVOCATIVE)
- アイテムを宣言するマクロが属性と衝突しない (C-MACRO-ATTR)
- アイテムを宣言するマクロがアイテムを宣言できる場所のどこでも使える (C-ANYWHERE)
- アイテムを宣言するマクロが可視性の指定をサポートしている (C-MACRO-VIS)
- 型の指定が柔軟である (C-MACRO-TY)
- ドキュメンテーション (クレートに十分なドキュメントが付けられている)
- クレートレベルにコード例付きの詳細なドキュメントがある (C-CRATE-DOC)
- 全てのアイテムにコード例が付いている (C-EXAMPLE)
- コード例が
try!やunwrapではなく?を使っている (C-QUESTION-MARK) - 関数のドキュメントにエラー、パニック、安全性に関する事項が含まれている (C-FAILURE)
- 文章に関係する項目へのリンクを含める (C-LINK)
- Cargo.tomlが一般的なメタデータを全て含んでいる (C-METADATA)
- authors, description, license, homepage, documentation, repository, readme, keywords, categories
- html_root_url属性が"https://docs.rs/CRATE/X.Y.Z"に設定されている (C-HTML-ROOT)
- 大きな変更が全てリリースノートに記載されている (C-RELNOTES)
- 無用な実装詳細がRustdocに表示されていない (C-HIDDEN)
- 予測性 (クレートを使い、見かけ通りに動作する読みやすいコードが書ける)
- スマートポインタがinherentメソッドを持っていない (C-SMART-PTR)
- 変換メソッドが最も関係の深い型に付いている (C-CONV-SPECIFIC)
- 明確なレシーバを持つ関数がメソッドになっている (C-METHOD)
- 関数がoutパラメータを持たない (C-NO-OUT)
- 奇妙な演算子オーバーロードを行っていない (C-OVERLOAD)
-
DerefとDerefMutを実装しているのはスマートポインタだけである (C-DEREF) - コンストラクタはスタティックなinherentメソッドである (C-CTOR)
- 柔軟性 (クレートが実用的なユースケースを幅広くカバーしている)
- 重複した処理を行わなくて済むように中間生成物を公開している (C-INTERMEDIATE)
- 呼び出し側がデータをコピーするタイミングを決める (C-CALLER-CONTROL)
- ジェネリクスを用いて関数の引数に対する制限を最小にしている (C-GENERIC)
- トレイトオブジェクトとして有用なトレイトはオブジェクトセーフになっている (C-OBJECT)
- 型安全性 (クレートが型システムを有効に活用している)
- newtypeを使って静的に値を区別する (C-NEWTYPE)
-
boolやOptionの代わりに意味のある型を使っている (C-CUSTOM-TYPE) - フラグの集合を列挙型ではなく
bitflagsで表している (C-BITFLAG) - 複雑な値の生成にビルダーパターンを使っている (C-BUILDER)
- 信頼性 (クレートが間違ったことをしない)
- 関数が引数を検証している (C-VALIDATE)
- デストラクタが失敗しない (C-DTOR-FAIL)
- ブロックする可能性のあるデストラクタには代替手段を用意する (C-DTOR-BLOCK)
- デバッガビリティ (クレートが容易なデバッグを支援している)
- 全てのパブリックな型に
Debugを実装する (C-DEBUG) -
Debug表現を空にしない (C-DEBUG-NONEMPTY)
- 全てのパブリックな型に
- 将来性 (クレートを、ユーザのコードを壊すことなく改善できる)
- sealedトレイトを使って下流の実装を適切に防いでいる (C-SEALED)
- 構造体のフィールドを適切にプライベートにする (C-STRUCT-PRIVATE)
- newtypeを用いて実装詳細を隠蔽している (C-NEWTYPE-HIDE)
- データ構造にderiveしたトレイトの境界を定義で繰り返さない (C-STRUCT-BOUNDS)
- 必要事項 (ある状況下では重要な問題)
- stableなクレートのパブリックな依存クレートがstableである (C-STABLE)
- クレートとその依存先がpermissiveなライセンスの下にある (C-PERMISSIVE)
命名
大文字・小文字の使い分けがRFC430に従っている (C-CASE)
Rustにおける基本的な命名規則はRFC 430に記述されています。
Rustは「型レベル」のもの(型やトレイト)にCamelCase、
「値レベル」のものにsnake_caseを使用する傾向があります。
正確には、次表のように命名します。
| アイテム | 規則 |
|---|---|
| クレート | 不明 |
| モジュール | snake_case |
| 型 | CamelCase |
| トレイト | CamelCase |
| Enumのバリアント | CamelCase |
| 関数 | snake_case |
| メソッド | snake_case |
| 一般のコンストラクタ | new または with_more_details |
| 変換を行うコンストラクタ | from_some_other_type |
| マクロ | snake_case! |
| ローカル変数 | snake_case |
| スタティック変数 | SCREAMING_SNAKE_CASE |
| 定数 | SCREAMING_SNAKE_CASE |
| 型パラメータ | 簡潔なCamelCase、大抵は大文字で1文字: T |
| ライフタイム | 短いlowercase、大抵は1文字: 'a, 'de, 'src |
| Feature | 不明、ただしC-FEATUREを参照 |
頭字語や複合語は、CamelCaseでは一語に数えます。例えばUUIDではなくUuidを使い、USizeではなくUsize、StdInではなくStdinを使って下さい。
snake_caseではis_xid_startのように小文字にします。
snake_caseまたはSCREAMING_SNAKE_CASEでは、
それが最後の文字を除いて一文字で区切ってはいけません。
例えば、b_tree_mapではなくbtree_mapとしますが、PI2ではなくPI_2とします。
クレート名の前後に-rsや-rustを付けるべきではありません。
全てのクレートがRust製であることは分かりきっています!
そのことをユーザに常に知らせ続ける必要はありません。
(訳注: レポジトリ名等ではなく、Cargo.tomlで指定するクレート名についての指針です)
標準ライブラリでの例
この規則は標準ライブラリの全体に渡って使用されています。
変換メソッドにas_, to_, into_を使っている (C-CONV)
変換メソッドの名前には次のプレフィクスを付けるべきです。
| プレフィクス | コスト | 所有権 |
|---|---|---|
as_ | 低い | 借用 -> 借用 |
to_ | 高い | 借用 -> 借用 借用 -> 所有 (Copyでない型) 所有 -> 所有 (Copy型) |
into_ | 可変 | 所有 -> 所有 (Copyでない型) |
以下に例を挙げます。
str::as_bytes()はコストなしに、strをUTF-8バイト列として見たビューを返します。 入力は借用された&strで出力は借用された&[u8]です。Path::to_strはOSの与えるパスのバイト列に対し、コストの高いUTF-8チェックを行います。 入出力はともに借用されています。小さくない実行時コストがあるため、これをas_strと呼ぶことはできません。str::to_lowercase()はUnicode規格に沿ってstrを小文字に変換したものを返します。 この処理は文字列のデコードを含み、またメモリの確保も行うでしょう。 入力は借用された&strで出力は所有されたStringです。f64::to_radians()は浮動小数点数で表された角度を度からラジアンに変換します。 入力はf64です。これが&f64でないのは、コピーに殆どコストが掛からないためです。 入力が消費されないので、このメソッドをinto_radiansと呼ぶのはミスリーディングです。String::into_bytes()はStringがラップしているVec<u8>を取り出します。 この処理にコストは掛かりません。このメソッドはStringの所有権を得て、所有されたVec<u8>を返します。BufReader::into_inner()はバッファリングされたreaderの所有権を得て、ラップされていたreaderを取り出します。 この処理にコストは掛かりません。バッファ内のデータは破棄されます。BufWriter::into_inner()はバッファリングされたwriterの所有権を得て、ラップされていたwriterを取り出します。 この処理にはコストの掛かるバッファのフラッシングが必要なことがあります。
as_やinto_の付くような変換メソッドは一般に抽象を弱めます。
内部表現を公開したり(as)、データを内部表現に分解したり(into)することになるからです。
一方で、to_と付くような変換メソッドは一般に抽象レベルを保てることが多いですが、
内部で何らかの処理を行いデータの表現方法を変換する必要があります。
ある値をラップして高レベルの意味を持たせるような型において、
ラップされた型にアクセスさせる手段はinto_inner()メソッドによって提供されるべきです。
これは例えば、バッファリングを提供するBufReaderや、
エンコード・デコードを行うGzDecoder、
アトミックなアクセスを提供するAtomicBoolといった型のようなセマンティクスを持つ型に適用できます。
変換メソッドの名前にmutを含めるときは、返り値の型の記述と同じ順番にしてください。
例えばVec::as_mut_sliceはミュータブルは名前の通りスライスを返します。
例えばas_slice_mut等よりもこのような命名が推奨されます。
# #![allow(unused_variables)] #fn main() { // Return type is a mut slice. fn as_mut_slice(&mut self) -> &mut [T]; #}
標準ライブラリでのさらなる例
Getterの名前がRustの規則に従っている (C-GETTER)
後述するような例外を除き、getterの名前をget_で始めるべきではありません。
# #![allow(unused_variables)] #fn main() { pub struct S { first: First, second: Second, } impl S { // Not get_first. pub fn first(&self) -> &First { &self.first } // Not get_first_mut, get_mut_first, or mut_first. pub fn first_mut(&mut self) -> &mut First { &mut self.first } } #}
getという命名は、getterによって得られるものが自明である場合にのみ使用されるべきです。
例えば、Cell::getはCellの中身を返します。
境界検査といった、実行時のバリデーションが必要なgetterには、
unsafeな_unchecked版も用意できないか検討してください。
そのようなメソッドの宣言はふつう、以下のようになります。
# #![allow(unused_variables)] #fn main() { fn get(&self, index: K) -> Option<&V>; fn get_mut(&mut self, index: K) -> Option<&mut V>; unsafe fn get_unchecked(&self, index: K) -> &V; unsafe fn get_unchecked_mut(&mut self, index: K) -> &mut V; #}
getterと変換(C-CONV)の間の違いは往々にして微かなもので、
常に境界がハッキリしている訳ではありません。
例えばTempDir::pathは一時ディレクトリのファイルシステム上のパスのgetterとして捉えられますが、
TempDir::into_pathは一時ディレクトリを削除する責任を呼び出し側に移す変換メソッドです。
このような場合pathはgetterなので、get_pathやas_pathと呼ぶことは正しくありません。
標準ライブラリでのさらなる例
std::io::Cursor::get_mutstd::ptr::Unique::get_mutstd::sync::PoisonError::get_mutstd::sync::atomic::AtomicBool::get_mutstd::collections::hash_map::OccupiedEntry::get_mut<[T]>::get_unchecked
イテレータを生成するメソッドの名前がiter, iter_mut, into_iterとなっている ([C-ITER])
RFC 199に従ってください。
Uという型の要素を持つコンテナの場合、イテレータを生成するメソッドは次のように命名するべきです。
# #![allow(unused_variables)] #fn main() { fn iter(&self) -> Iter // Iter implements Iterator<Item = &U> fn iter_mut(&mut self) -> IterMut // IterMut implements Iterator<Item = &mut U> fn into_iter(self) -> IntoIter // IntoIter implements Iterator<Item = U> #}
この指針は全ての要素が同質であると意味付けられたコレクションに対して適用されます。
例えばstrはバイト列ですが、有効なUTF-8であるという保証があるため、この指針は適用できません。
従ってiter/iter_mut/into_iterといったメソッド群ではなく、
バイト列としてイテレートするstr::bytes、キャラクタ列としてイテレートするstr::charsを持ちます。
このガイドラインはメソッドにのみ適用され、関数は対象外です。
例えばurlクレートのpercent_encodeは文字列をパーセントエンコーディングしていくイテレータを返します。
この場合、iter/iter_mut/into_iterといった命名を使うことに利点はありません。
標準ライブラリでの例
イテレータの型名が、それを生成するメソッドと揃っている (C-ITER-TY)
into_iterというメソッドはIntoIterという型を返すべきです。
他のイテレータを返すメソッドでも同様です。
この指針は主にメソッドに対して適用されますが、関数に対しても大抵は適用できます。
例えばurlクレートのpercent_encode関数はPercentEncode
という型名のイテレータを返します。
このような命名法はvec::IntoIterのようにモジュール名を付けて呼ぶ際に最も有用です。
標準ライブラリでの例
Vec::iterはIterを返す。Vec::iter_mutはIterMutを返す。Vec::into_iterはIntoIterを返す。BTreeMap::keysはKeysを返す。BTreeMap::valuesはValuesを返す。
Featureの名前に余計な単語が入っていない (C-FEATURE)
Cargoのfeatureの名前に意味のない単語を付けないでください。
use-abcやwith-abcなどとせず、単にabcとするべきです。
標準ライブラリへの依存がオプションである場合が最もよく目にする例でしょう。 これを指針に従って行うと、以下のようになります。
# In Cargo.toml
[features]
default = ["std"]
std = []
# #![allow(unused_variables)] #fn main() { // In lib.rs #![cfg_attr(not(feature = "std"), no_std)] #}
このfeatureにstd以外の、use-stdやwith-stdあるいはその他の独創的な名前を付けないでください。
そうすることで、Cargoが暗黙的に追加するオプショナルな依存性のfeatureと沿った形になります。
例えばxというクレートがSerdeと標準ライブラリに対する依存をオプションとして持つとき、
[package]
name = "x"
version = "0.1.0"
[features]
std = ["serde/std"]
[dependencies]
serde = { version = "1.0", optional = true }
のようになります。そして、さらにxに依存するとき、Serdeへの依存を
features = ["serde"]で有効化できます。
また、同じように標準ライブラリへの依存をfeatures = ["std"]で有効化できます。
Cargoによって暗黙的に追加されるfeatureはserdeであり、use-serdeでもwith-serdeでもありません。
ですから、明示的なfeatureに対しても同じようにするべきなのです。
関連事項として、Cargoのfeatureは追加式ですから、
no-abcといったfeatureは大きな間違いです。
命名時に単語を並べる順番が揃っている (C-WORD-ORDER)
標準ライブラリにおけるエラー型をいくつか示します。
JoinPathsErrorParseBoolErrorParseCharErrorParseFloatErrorParseIntErrorRecvTimeoutErrorStripPrefixError
全て、動詞-オブジェクト-エラーの順番で並んでいます。
もし新たにアドレスのパースに失敗したことを表すエラーを追加するならば、
AddrParseError等ではなく、
一貫性を考えて動詞-オブジェクト-エラーの順に並べParseAddrErrorとすべきです。
どの順番を選ぶかは大して重要ではありませんが、クレート内での一貫性、 標準ライブラリの似た機能との整合性には注意してください。
相互運用性
積極的に一般的なトレイトを型に実装している (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を代わりに渡せるというのが答えです。
例
flate2::read::GzDecoder::newflate2::write::GzEncoder::newserde_json::from_readerserde_json::to_writer
マクロ
入力の構文から結果をイメージできるようになっている (C-EVOCATIVE)
Rustのマクロの入力ではお好きな構文を実装することができますが、 特異なシンタックスを導入するのではなく、 Rustの構文に似せることでマクロ以外のコードとの統一性を保てるようにしてください。 キーワードや記号類の選択・配置には注意してください。
良い指針は、マクロの出力に似せた構文(特にキーワードと記号類)を使用することです。
例えば、マクロが与えられた名前の構造体型を宣言するならば、
マクロの入力において名前の前にキーワードstructを付けさせるようにし、
コードを読む人に対して構造体型が宣言されるということを知らせてください。
# #![allow(unused_variables)] #fn main() { // このようにしてください... bitflags! { struct S: u32 { /* ... */ } } // ...キーワードがなかったり... bitflags! { S: u32 { /* ... */ } } // ...その場その場で勝手な単語を導入したりしないでください bitflags! { flags S: u32 { /* ... */ } } #}
もう一つの例はセミコロンとコンマの問題です。 Rustにおける定数の宣言では、最後にセミコロンを付けます。 複数の定数を宣言するマクロの場合、Rustの構文と違っているとしても同様にセミコロンを付けるようにすべきでしょう。
# #![allow(unused_variables)] #fn main() { // 普通の定数の宣言にはセミコロンが使われます const A: u32 = 0b000001; const B: u32 = 0b000010; // なので、こうすべきです... bitflags! { struct S: u32 { const C = 0b000100; const D = 0b001000; } } // ...こうではなく bitflags! { struct S: u32 { const E = 0b010000, const F = 0b100000, } } #}
マクロには幅広い用例があるため、こういった特定の例はそのまま通用しないでしょう。 ですが、同じような考え方を適用できないか考えてみて下さい。
アイテムを宣言するマクロが属性と衝突しない (C-MACRO-ATTR)
アイテムを宣言するマクロは、それぞれに属性を付与できるようになっているべきです。 よくある例は特定のアイテムをcfgで条件コンパイルするような場合です。
# #![allow(unused_variables)] #fn main() { bitflags! { struct Flags: u8 { #[cfg(windows)] const ControlCenter = 0b001; #[cfg(unix)] const Terminal = 0b010; } } #}
構造体型や列挙型を生成するマクロも、deriveを使えるように属性をサポートすべきです。
# #![allow(unused_variables)] #fn main() { bitflags! { #[derive(Default, Serialize)] struct Flags: u8 { const ControlCenter = 0b001; const Terminal = 0b010; } } #}
アイテムを宣言するマクロがアイテムを宣言できる場所のどこでも使える (C-ANYWHERE)
Rustではアイテムをモジュールレベルから関数のような狭いスコープまで配置することができます。 アイテムを宣言するマクロも同様に、いずれの場所でも動くようになっているべきです。 そして、少なくともモジュールレベルおよび関数レベルでマクロを呼び出すテストを追加すべきです。
# #![allow(unused_variables)] #fn main() { #[cfg(test)] mod tests { test_your_macro_in_a!(module); #[test] fn anywhere() { test_your_macro_in_a!(function); } } #}
モジュールスコープでは動くものの、関数スコープでは動かないマクロの簡単な例を挙げます。
# #![allow(unused_variables)] #fn main() { macro_rules! broken { ($m:ident :: $t:ident) => { pub struct $t; pub mod $m { pub use super::$t; } } } broken!(m::T); // 問題なくTおよびm::Tに展開される fn g() { broken!(m::U); // コンパイルに失敗する、super::Uがgではなく外側のモジュールの方を指すため } #}
アイテムを宣言するマクロが可視性の指定をサポートしている (C-MACRO-VIS)
マクロによって宣言されるアイテムの可視性は、Rustの可視性の構文に沿ってください。
デフォルトではプライベートで、pubが指定されたらパブリックにします。
# #![allow(unused_variables)] #fn main() { bitflags! { struct PrivateFlags: u8 { const A = 0b0001; const B = 0b0010; } } bitflags! { pub struct PublicFlags: u8 { const C = 0b0100; const D = 0b1000; } } #}
型の指定が柔軟である (C-MACRO-TY)
マクロが$t:tyのようにして型を受け取る場合、
以下の全てに対応できるべきです。
- プリミティブ型:
u8,&str - 相対パス:
m::Data - 絶対パス:
::base::Data - 上位を参照する相対パス:
super::Data - ジェネリクス:
Vec<String>
これができないマクロの簡単な例を挙げます。 次のマクロはプリミティブ型や絶対パスではうまく動きますが、相対パスでは動きません。
# #![allow(unused_variables)] #fn main() { macro_rules! broken { ($m:ident => $t:ty) => { pub mod $m { pub struct Wrapper($t); } } } broken!(a => u8); // okay broken!(b => ::std::marker::PhantomData<()>); // okay struct S; broken!(c => S); // fails to compile #}
ドキュメンテーション
クレートレベルにコード例付きの詳細なドキュメントがある (C-CRATE-DOC)
RFC 1687を参照してください。
全てのアイテムにコード例が付いている (C-EXAMPLE)
全てのパブリックなモジュール、トレイト、構造体型、列挙型、関数、メソッド、マクロ、 およびtypeエイリアスに、その機能を示すコード例を含むrustdocドキュメントが存在するべきです。
この指針は無理のない範囲で適用してください。
適用可能な、その他のアイテムにおけるコード例へのリンクでも十分でしょう。 例えば、ある関数がある型を使うとして、 コード例が関数と型のどちらか一方にあれば、もう一方からはリンクするだけで十分です。
コード例の目的がどのようにそのアイテムを使うかを示すことであるとは限りません。 読者は関数の呼び出し方、列挙型に対するmatchの使い方といった基本的な事柄は理解していると期待できます。 ですから、コード例の目的はなぜアイテムを使うべきかの提示であることも多くあります。
// これはclone()を使うコード例の不味い例です。 // *どう*clone()を呼ぶのか機械的に示しているだけであり、 // *なぜ*これを使うべきかがまったく示されていません。 fn main() { let hello = "hello"; hello.clone(); }
コード例がtry!やunwrapではなく?を使っている (C-QUESTION-MARK)
ユーザがコード例を丸写しすることは、その良し悪しは別としてよくあることです。 そして、エラーをunwrapするか否かという判断はユーザが自覚的に判断すべきことです。
エラー処理を含むコード例を構成する一般的な方法を以下に示します。
#で始まる行はcargo testによってコンパイルされますが、
ユーザに見えるrustdocには表示されません。
/// ```rust
/// # use std::error::Error;
/// #
/// # fn try_main() -> Result<(), Box<Error>> {
/// your;
/// example?;
/// code;
/// #
/// # Ok(())
/// # }
/// #
/// # fn main() {
/// # try_main().unwrap();
/// # }
/// ```
関数のドキュメントにエラー、パニック、安全性に関する事項が含まれている (C-FAILURE)
エラーとなる条件を"Errors"セクションに記載するべきです。 これはトレイトメソッドに対しても同様です。エラーを返す可能性のあるトレイトメソッドには "Errors"セクションを含んだドキュメントを付けるべきです。
標準ライブラリの例を挙げると、トレイトメソッドstd::io::Read::readの実装のいくつかは
エラーを返す可能性があります。
/// Pull some bytes from this source into the specified buffer, returning
/// how many bytes were read.
///
/// ... lots more info ...
///
/// # Errors
///
/// If this function encounters any form of I/O or other error, an error
/// variant will be returned. If an error is returned then it must be
/// guaranteed that no bytes were read.
また、パニックを起こす条件を"Panics"セクションに記載するべきです。 これはトレイトメソッドに対しても同様です。パニックを起こす可能性のあるトレイトメソッドには "Panics"セクションを含んだドキュメントを付けるべきです。
標準ライブラリを例にすると、パニックを起こす可能性のあるメソッドとしてVec::insertが挙げられます。
/// Inserts an element at position `index` within the vector, shifting all
/// elements after it to the right.
///
/// # Panics
///
/// Panics if `index` is out of bounds.
あらゆるパニックの可能性を網羅する必要はありません。
特に、呼び出し側の提供したロジックの内部でパニックが起こる場合、
例えば以下のような事例でDisplayのパニックをドキュメントに記載するのは過剰です。
ですが、微妙なケースではできる限り多くの可能性を網羅する方が良いでしょう。
# #![allow(unused_variables)] #fn main() { /// # Panics /// /// This function panics if `T`'s implementation of `Display` panics. pub fn print<T: Display>(t: T) { println!("{}", t.to_string()); } #}
unsafeな関数のドキュメントには、 その関数を正しく使うために呼び出し側が守らなければならない不変条件を記載した "Safety"セクションを含めてください。
例えば、unsafeな関数であるstd::ptr::readは以下の事項を呼び出し側に要求しています。
/// Reads the value from `src` without moving it. This leaves the
/// memory in `src` unchanged.
///
/// # Safety
///
/// Beyond accepting a raw pointer, this is unsafe because it semantically
/// moves the value out of `src` without preventing further usage of `src`.
/// If `T` is not `Copy`, then care must be taken to ensure that the value at
/// `src` is not used before the data is overwritten again (e.g. with `write`,
/// `zero_memory`, or `copy_memory`). Note that `*src = foo` counts as a use
/// because it will attempt to drop the value previously at `*src`.
///
/// The pointer must be aligned; use `read_unaligned` if that is not the case.
文章に関係する項目へのリンクを含める (C-LINK)
同じ型の別のメソッドへのリンクはふつう次のようになります。
[`serialize_struct`]: #method.serialize_struct
別の型へのリンクはこうなります。
[`Deserialize`]: trait.Deserialize.html
親・子モジュールへのリンクは次のようにします。
[`Value`]: ../enum.Value.html
[`DeserializeOwned`]: de/trait.DeserializeOwned.html
この指針はRFC 1574の"Link all the things"によって公式に推奨されています。
Cargo.tomlが一般的なメタデータを全て含んでいる (C-METADATA)
Cargo.tomlの[package]セクションは以下の値を含むべきです。
authorsdescriptionlicenserepositoryreadmekeywordscategories
加えて、2つの任意項目があります。
documentationhomepage
デフォルトで、crates.ioはdocs.rs上のドキュメントへリンクします。
documentationメタデータはdocs.rs以外の場所でドキュメントをホストしている場合にのみ必要です。
例えば、そのクレートがdocs.rsのビルド環境に存在しない共有ライブラリを要求している場合などです。
homepageメタデータはソースレポジトリやAPIドキュメント以外の独自のウェブサイトがある場合のみ
使用されるべきです。documentationやrepositoryと重複した値を入れないでください。
例えば、Serdeはhomepageを専用のウェブサイトであるhttps://serde.rsに設定しています。
html_root_url属性が設定されている (C-HTML-ROOT)
docs.rsを主なAPIドキュメントとして使っているならば"https://docs.rs/CRATE/MAJOR.MINOR.PATCH"に設定してください。
html_root_url属性は、rustdocが下流クレートをコンパイルする際にどのようにURLを作成するべきかを伝えるものです。
これが無ければ、あなたのクレートに依存するクレートにおいて、ドキュメント中のリンクが間違ったものになります。
# #![allow(unused_variables)] #![doc(html_root_url = "https://docs.rs/log/0.3.8")] #fn main() { #}
バージョンがURLに含まれていることから分かる通り、
Cargo.toml内のバージョン番号と同期させる必要があります。
html_root_urlがクレートのバージョンとずれた場合に失敗するインテグレーションテストを追加する
version-syncクレートが役に立つはずです。
この方式が気に入らなければ、Cargo.tomlのバージョンの所にメモ書きを残しておくとよいでしょう。
version = "0.3.8" # html_root_urlの更新 忘れない
docs.rs以外にドキュメントをホストしているなら、html_root_urlの値にクレート名 + index.htmlを付け足すと
あなたのクレートのルートモジュールにたどり着けるように設定してください。
例えば、ルートモジュールが"https://api.rocket.rs/rocket/index.html"にあるならば、
html_root_urlは"https://api.rocket.rs"とすべきです。
大きな変更が全てリリースノートに記載されている (C-RELNOTES)
クレートのユーザはリリースノートを読むことでバージョン間の変更点を知ることができます。 クレートレベルのドキュメントまたはCargo.tomlからリンクされたリポジトリにリリースノートへのリンクを置いてください。
破壊的変更(RFC 1105で定義されている)は明確に区別されているべきです。
Gitを用いてソースコードの履歴を管理しているなら、 crates.ioに発行された全てのリリースは対応するコミットにタグを付けてください。 Git以外のVCSでも同様の処理をしておくべきです。
# Tag the current commit
GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "Release 0.3.0" 0.3.0
git push --tags
アノテーション付きタグが一つでも存在するとアノテーションのないタグを無視するGitコマンドがあるため、 アノテーション付きタグの使用が推奨されます。
例
Rustdocはユーザがそのクレートを使用するのに必要な情報を網羅すべきですが、 それ以上の内容は含むべきではありません。 文章中で関係のある実装詳細を解説するのはよいですが、 実際にドキュメント項目として現れてはいけません。
特に、どのようなimplがドキュメントに表示されるかに関しては精選するようにしてください。
ユーザがクレートを使用するのに必要なものだけ示し、それ以外は隠しましょう。
以下のコードでは、PublicErrorのドキュメントにFrom<PrivateError>が表示されてしまいます。
ユーザがPrivateError型を扱うことはないため、この項目はユーザにとっては関係ないものです。
なので、#[doc(hidden)]を使って隠します。
# #![allow(unused_variables)] #fn main() { // This error type is returned to users. pub struct PublicError { /* ... */ } // This error type is returned by some private helper functions. struct PrivateError { /* ... */ } // Enable use of `?` operator. #[doc(hidden)] impl From<PrivateError> for PublicError { fn from(err: PrivateError) -> PublicError { /* ... */ } } #}
pub(crate)も実装詳細をパブリックなAPIから隠す便利なツールです。
同じモジュール以外からもアイテムを使えるようになりますが、他のクレートからは見えません。
予測性
スマートポインタがinherentメソッドを持っていない (C-SMART-PTR)
例えば、Box::into_rawは次のように定義されています。
# #![allow(unused_variables)] #fn main() { impl<T> Box<T> where T: ?Sized { fn into_raw(b: Box<T>) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; let ptr = Box::into_raw(boxed_str); #}
もしこれがinherentメソッドであったら、呼び出そうとしているメソッドがTのものなのか
Box<T>のものなのか区別が付かなくなります。
# #![allow(unused_variables)] #fn main() { impl<T> Box<T> where T: ?Sized { // Do not do this. fn into_raw(self) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; // スマートポインタのDerefを経由してstrのメソッドにアクセスしている boxed_str.chars() // これは`Box<str>`のメソッド……? boxed_str.into_raw() #}
変換メソッドが最も関係の深い型に付いている (C-CONV-SPECIFIC)
迷ったら_fromよりもto_/as_/into_を選んでください。
後者の方がより使いやすく、また他のメソッドにチェーンすることもできるからです。
2つの型の間の変換において、多くの場合どちらか一方が明らかに特徴的です。
すなわち、他方にはない不変条件や解釈が追加されています。
例えばstrはUTF-8でエンコードされたバイト列ですから、単なるバイト列である&[u8]より特徴的です。
変換メソッドは、関係する型の中で、より特徴的なものが持つべきです。
従って、strはas_bytesメソッド及びfrom_utf8コンストラクタを持つのです。
この方が直感的であるだけでなく、&[u8]のような型が無数の変換メソッドで汚染されていくという事態が避けられます。
明確なレシーバを持つ関数がメソッドになっている (C-METHOD)
特定の型と強く関連した操作についてはメソッドにしてください。
# #![allow(unused_variables)] #fn main() { impl Foo { pub fn frob(&self, w: widget) { /* ... */ } } #}
関数にしてはいけません。
# #![allow(unused_variables)] #fn main() { pub fn frob(foo: &Foo, w: widget) { /* ... */ } #}
関数でなくメソッドを選ぶことには多数の利点があります。
- インポートしたり関数へのパスを記述したりする必要がない。その型の値さえあれば必要な操作ができます。
- 呼び出し時に自動借用が働きます。 (可変借用も含めて)
- 「この型
Tで何ができるんだろう」という疑問への答えが簡単になります。 (特にrustdocを使用している場合) self記法が使われるため、より簡潔かつ明白に所有権の区別が示されます。
関数がoutパラメータを持たない (C-NO-OUT)
例えば複数のBarを返すときはこのようにしてください。
# #![allow(unused_variables)] #fn main() { fn foo() -> (Bar, Bar) #}
このようにoutパラメータのようなものを取ってはいけません。
# #![allow(unused_variables)] #fn main() { fn foo(output: &mut Bar) -> Bar #}
タプルや構造体を使って複数の値を返しても効率のよいコードにコンパイルされますし、 ヒープの確保も行われません。複数の値を返す必要があるならこれらの型を利用すべきです。
例外は関数が呼び出し側の所有するデータを変更する場合です。 例えば、バッファの再利用をする場合は次のようになるでしょう。
# #![allow(unused_variables)] #fn main() { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> #}
奇妙な演算子オーバーロードを行っていない (C-OVERLOAD)
組み込みの演算子(*や|など)はstd::opsにあるトレイトを実装することで使えるようになります。
これらの演算子には元から意味が付与されています。
例えば、Mulは乗算のような(そして結合性などの特性を共有した)演算にのみ実装されるべきです。
DerefとDerefMutを実装しているのはスマートポインタだけである (C-DEREF)
Derefトレイトはコンパイラによって様々な状況で暗黙的に使われ、メソッドの解決と関わります。
その周辺の規則はスマートポインタを念頭において設計されているため、
これらのトレイトはスマートポインタに対してのみ実装されるべきです。
標準ライブラリでの例
コンストラクタはスタティックなinherentメソッドである (C-CTOR)
Rustにおいて、「コンストラクタ」は単なる慣習に過ぎません。 コンストラクタの命名には様々な慣習があり、その区別が分かり辛いことが多々あります。
最も基本的なコンストラクタの形は引数のないnewメソッドです。
# #![allow(unused_variables)] #fn main() { impl<T> Example<T> { pub fn new() -> Example<T> { /* ... */ } } #}
コンストラクタは、その生成する型のスタティック(selfを取らない)なinherentメソッドです。
型をインポートする慣習と併せれば、分かりやすく簡潔にその型を生成することができます。
# #![allow(unused_variables)] #fn main() { use example::Example; // Construct a new Example. let ex = Example::new(); #}
newという名前は、1つ目の最も重要なコンストラクタに使われるべきです。
上記の例のように引数を取らないこともあれば、Boxに入れる値を取るBox::newのように、
引数を取ることもあります。
主にI/Oリソースを表す型では、File::open、Mmap::open、
TcpStream::connect、あるいは UpdSocket::bindのように
コンストラクタの命名が異なっていることがあります。
これらは、各々の領域で適した名前が選ばれています。
ある型の値を生成する方法が複数存在することは多くあります。
そういった場合、2個目以降のコンストラクタには_with_fooなどと名前の最後に付けることが一般的です。
例えば、Mmap::open_with_offsetなどです。
複数のオプションがあるならばビルダーパターン(C-BUILDER)の使用も考えてください。
別の型の値を取って変換を行うコンストラクタというものもあります。
それらはstd::io::Error::from_raw_os_errorのように、一般にfrom_から始まる名前を持ちます。
ここで、よく似たものにFromトレイト(C-CONV-TRAITS)が存在します。
from_の付いた変換コンストラクタとFrom<T>実装の間には3つの相違点があります。
from_コンストラクタはunsafeにすることができますが、Fromの実装ではできません。 例:Box::from_raw。from_コンストラクタは、u64::from_str_radixのように 元になるデータを区別するための追加の引数を取ることができます。From実装は元のデータから出力の型のエンコード方法を決定できる場合にのみ適しています。u64::from_beやString::from_utf8のように入力の型が単なるデータ列であるとき、 コンストラクタの名前によってその意味を伝えることが可能です。
標準ライブラリでの例
std::io::Error::newはIOエラーの生成に使われるコンストラクタstd::io::Error::from_raw_os_errorは OSから与えられたエラーコードを変換するコンストラクタBox::newは引数を1つとり、コンテナ型を生成するコンストラクタFile::openはファイルをオープンするMmap::open_with_offsetは指定されたオプションでメモリーマップをオープンする
柔軟性
重複した処理を行わなくて済むように中間生成物を公開している (C-INTERMEDIATE)
何らかの処理を行って結果を返す関数の多くは、関連したデータを途中で生成しています。 もしユーザにとって有益なものがあれば、それを公開するAPIの追加を検討してください。
標準ライブラリでの例
-
Vec::binary_searchは目的の値が見つかったか否かをboolで表したり、 目的の値が見つかった位置をOption<usize>で返すようにはなっていません。 代わりに、もし見つかればその位置を、そして見つからなければその値が挿入されるべき位置を返します。 -
String::from_utf8は入力が正しいUTF-8でなければ失敗します。 そのとき、入力のどこまでが正しいUTF-8であったかを返し、また入力されたバイト列の所有権も返します。 -
HashMap::insertはそのキーの場所に元から値が存在していたら、その値をOption<T>で返します。 ユーザがこの値を必要としているとき、この挙動がなければハッシュテーブルへのルックアップを二度繰り返さなければなりません。
呼び出し側がデータをコピーするタイミングを決める (C-CALLER-CONTROL)
引数の所有権を要する関数は、借用して複製するのではなく所有権を受け取るべきです。
# #![allow(unused_variables)] #fn main() { // 良い例: fn foo(b: Bar) { /* use b as owned, directly */ } // 悪い例: fn foo(b: &Bar) { let b = b.clone(); /* use b as owned after cloning */ } #}
もし関数が引数の所有権を必要としないのなら、 所有権を取って最終的にdropする代わりに可変あるいは非可変借用を受け取るべきです。
# #![allow(unused_variables)] #fn main() { // 良い例: fn foo(b: &Bar) { /* use b as borrowed */ } // 悪い例: fn foo(b: Bar) { /* use b as borrowed, it is implicitly dropped before function returns */ } #}
Copyトレイトは本当に必要な場合のみ使用してください。
単に低コストでコピーが可能であると伝えるために使用してはいけません。
ジェネリクスを用いて関数の引数に対する制限を最小にしている (C-GENERIC)
関数の引数に対する制約が少ないほど、その関数は幅広く使えるようになります。
単にイテレーションが必要なだけであれば、このようにジェネリクスを用いてください。
# #![allow(unused_variables)] #fn main() { fn foo<I: IntoIterator<Item = i64>>(iter: I) { /* ... */ } #}
特定の型を指定しないでください。
# #![allow(unused_variables)] #fn main() { fn foo(c: &[i64]) { /* ... */ } fn foo(c: &Vec<i64>) { /* ... */ } fn foo(c: &SomeOtherCollection<i64>) { /* ... */ } #}
もっと言えば、ジェネリクスを用い、関数の引数に対して必要な制約を正確に示してください。
ジェネリクスの利点
-
再利用性。ジェネリックな関数は受け取る型への明確な要件を示しつつ、 多くの型に対して適用できます。
-
スタティックディスパッチと最適化。ジェネリック関数を呼び出すと特定の型に特殊化("monomorphized")されます。 トレイトメソッドの呼び出しはスタティックかつ実装を直接呼び出す形に変換され、 コンパイラは呼び出しをインライン化し最適化することが可能です。
-
レイアウトのインライン化。構造体型や列挙型がジェネリックな型
Tを持つとき、 型Tの値へは間接的なアクセスを挟むことなく、構造体型や列挙型の内部にインライン化されます。 -
推論。ジェネリック関数の型パラメータは大抵推論が可能であるため、 明示的な変換やその他のメソッド呼び出しといったコード中の冗長な部分を削減することが可能です。
-
正確な型。ジェネリクスによって型に名前を与えられるため、 正確にその型を受け取りあるいは生成する箇所を指定することができます。 例えば、次の関数は全く同じ型
Tを受け取り、返すことが保証されます。Traitを実装する異なる型を用いて呼び出したりすることはできません。# #![allow(unused_variables)] #fn main() { fn binary<T: Trait>(x: T, y: T) -> T #}
ジェネリクスの欠点
-
コードサイズ。ジェネリック関数の特殊化により、関数の中身は複製されます。 コードサイズの増大がパフォーマンスと釣り合うかどうか検討するべきです。
-
一様な型。これは型が正確であることの裏返しです。型パラメータ
Tは一つの実際の型をもちます。 例えばVec<T>は単一の具体型のコレクションです(そして内部的にはメモリ上に隣り合って並べられます)。 一様でないコレクションが有用な場合もあります。trait objectsを参照してください。 -
関数の定義の複雑化。ジェネリクスを多用すると、関数の定義を読んだり理解することが難しくなります。
標準ライブラリの例
std::fs::File::openはAsRef<Path>というジェネリックな型を取ります。 これにより、文字列リテラル"f.txt"やPath、あるいはOsStringなどを渡して ファイルを開くことができます。
トレイトオブジェクトとして有用なトレイトがオブジェクトセーフになっている (C-OBJECT)
トレイトオブジェクトには大きな制約があります。それは、トレイトオブジェクト経由で呼ばれるメソッドは
ジェネリクスを使用できず、またSelfをレシーバ以外の引数で使用できないことです。
トレイトを設計する際、そのトレイトがオブジェクトとして使用されるのか ジェネリクスの境界として使用されるのかを決めておく必要があります。
そのトレイトがオブジェクトとして使用されることを念頭に置くならば、 トレイトメソッドではジェネリクスの代わりにトレイトオブジェクトを使用すべきです。
Self: Sizedというwhere節を用いることで、特定のメソッドをトレイトオブジェクトから除外することができます。
次のトレイトはジェネリックなメソッドを持つためオブジェクトセーフではありません。
# #![allow(unused_variables)] #fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T); } #}
ジェネリックなメソッドにSelf: Sizedを要求させることでトレイトオブジェクトから除外し、
そのトレイトをオブジェクトセーフにすることが可能です。
# #![allow(unused_variables)] #fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T) where Self: Sized; } #}
トレイトオブジェクトの利点
- 一様性。これ無しでは解決できない問題もあります。
- コードサイズ。ジェネリクスと異なり、トレイトオブジェクトは特殊化(monomorphized)されたコードを生成しないため、 コードサイズを大幅に削減することができます。
トレイトオブジェクトの欠点
- ジェネリックなメソッドが使えない。トレイトオブジェクトは今のところジェネリックなメソッドを 持つことができません。
- 動的ディスパッチとファットポインタ。トレイトオブジェクトは、パフォーマンスに影響する可能性のある 間接アクセスと仮想関数テーブルによるディスパッチを引き起こします。
- Selfが使えない。メソッドのレシーバ引数を除いて
Self型を取ることはできません。
標準ライブラリでの例
io::Readとio::Writeは頻繁にオブジェクトとして使われます。Iteratorには、トレイトオブジェクトとして使用できるようにするため、Self: Sizedが指定されたジェネリックなメソッドがあります。
型安全性
newtypeを使って静的に値を区別する (C-NEWTYPE)
実際の型は同じでも、newtypeを用いることでその異なる解釈の間を静的に区別することができます。
例えば、f64の値はマイルとキロメートルの何れかの量を示しているかもしれません。
newtypeを使うことで、どちらが正しい解釈なのかを示すことが可能です。
# #![allow(unused_variables)] #fn main() { struct Miles(pub f64); struct Kilometers(pub f64); impl Miles { fn to_kilometers(self) -> Kilometers { /* ... */ } } impl Kilometers { fn to_miles(self) -> Miles { /* ... */ } } #}
このように型を分けることで、混ざってしまわないことを静的に保証できます。 例えば、
# #![allow(unused_variables)] #fn main() { fn are_we_there_yet(distance_travelled: Miles) -> bool { /* ... */ } #}
は間違ってKilometersの値で呼ばれることはありません。
コンパイラが適切な変換を施すことを思い出させてくれるため、恐ろしいバグも回避できます。
boolやOptionの代わりに意味のある型を使っている (C-CUSTOM-TYPE)
こうして下さい。
# #![allow(unused_variables)] #fn main() { let w = Widget::new(Small, Round) #}
次は駄目な例です。
# #![allow(unused_variables)] #fn main() { let w = Widget::new(true, false) #}
boolやu8、Optionのような汎用的な型には様々な解釈が考えられます。
専用の型(列挙型、構造体、あるいはタプル)を用い、その意味と不変条件を伝えるようにしてください。
上記の駄目な例では、引数の名前を調べない限りtrueとfalseがどういった意味なのか分かりません。
SmallやRoundといった型を使った場合ではその意味が明確になっています。
専用の型を使うことで今後の拡張も楽になります。上記の例で言うと、
ここにExtraLargeというバリアントを追加したりできますね。
既存の型にゼロコストで区別された名前を付ける方法についてはnewtypeパターン(C-NEWTYPE)を参照してください。
フラグの集合を列挙型ではなくbitflagsで表している (C-BITFLAG)
Rustではenumにおいて値を明示することが可能です。
# #![allow(unused_variables)] #fn main() { enum Color { Red = 0xff0000, Green = 0x00ff00, Blue = 0x0000ff, } #}
値の指定は他のシステムや言語向けに整数値としてシリアライゼーションしないといけない場合に便利です。
数値でなくColorを関数が取れば、数値への変換が可能であると同時に不正な値の入力が防げるため
「型安全性」に貢献します。
enumは複数の選択肢のうちの1つを要求するAPIに使われます。
しかし、APIの入力がフラグの集合である場合もあります。
C言語のコードでは各々のフラグを特定のビットに割当てることで、1つの整数値によって32あるいは64等のフラグを表せるようにします。
Rustではbitflagsクレートがこのパターンの型安全な実装を提供しています。
#[macro_use] extern crate bitflags; bitflags! { struct Flags: u32 { const FLAG_A = 0b00000001; const FLAG_B = 0b00000010; const FLAG_C = 0b00000100; } } fn f(settings: Flags) { if settings.contains(FLAG_A) { println!("doing thing A"); } if settings.contains(FLAG_B) { println!("doing thing B"); } if settings.contains(FLAG_C) { println!("doing thing C"); } } fn main() { f(FLAG_A | FLAG_C); }
複雑な値の生成にビルダーパターンを使っている (C-BUILDER)
以下のような理由によって、データ構造の生成に複雑な手順が必要なことがあります。
- 多数の入力がある
- 複合的なデータである (例えばスライス)
- オプショナルな設定データがある
- 選択肢が複数ある
こんなとき、異なった引数をもつ多数のコンストラクタを追加してしまいがちです。
そういったデータ構造Tがあるとき、Tに ビルダー を用意することを検討してください。
- インクリメンタルに
Tを設定するための別のデータ型、TBuilderを作ります。 可能であれば、より明瞭な名前を選んでください。例えば、子プロセスを作成するビルダーのCommand、Urlを作成するビルダーのParseOptionsのようにです。 - ビルダー自体のコンストラクタは、
Tを生成するために 必須 のパラメータのみ取るべきです。 - ビルダーは設定を行うための便利なメソッドを提供するべきです。
例えば、複合的なデータをインクリメンタルに設定できるようなものです。
メソッドは
selfを返して、メソッドチェーンを行えるようにすべきです。 - ビルダーは実際に
Tの値を生成する「終端」メソッドを1つ以上提供すべきです。
ビルダーパターンは特に、Tの生成においてプロセスやタスクの立ち上げのような副作用が生じる場合にも適しています。
Rustでのビルダーパターンの作り方には、所有権の取り扱いによって以下で示す2通りの方法があります。
非消費ビルダー (推奨)
最終的なTの生成にビルダー自身を必要としない場合があります。
例としてstd::process::Commandを挙げます。
# #![allow(unused_variables)] #fn main() { // NOTE: 実際のCommand APIはStringを所有しません。 // これは単純化されたバージョンです pub struct Command { program: String, args: Vec<String>, cwd: Option<String>, // etc } impl Command { pub fn new(program: String) -> Command { Command { program: program, args: Vec::new(), cwd: None, } } /// Add an argument to pass to the program. pub fn arg(&mut self, arg: String) -> &mut Command { self.args.push(arg); self } /// Add multiple arguments to pass to the program. pub fn args(&mut self, args: &[String]) -> &mut Command { self.args.extend_from_slice(args); self } /// Set the working directory for the child process. pub fn current_dir(&mut self, dir: String) -> &mut Command { self.cwd = Some(dir); self } /// Executes the command as a child process, which is returned. pub fn spawn(&self) -> io::Result<Child> { /* ... */ } } #}
ビルダーの設定データを使って実際にプロセスを立ち上げるspawnメソッドが、
ビルダーを不変参照で受け取っていることに注目してください。
これはプロセスの立ち上げにビルダーの設定データの所有権が必要ないからです。
終端メソッドであるspawnが参照しか必要としないのですから、
設定メソッドはselfを参照で受け取り、返すべきです。
利点
借用を使うことで、Commandはワンライナーでも、もっと複雑なことをする場合でも
どちらにでも使うことができます。
# #![allow(unused_variables)] #fn main() { // One-liners Command::new("/bin/cat").arg("file.txt").spawn(); // Complex configuration let mut cmd = Command::new("/bin/ls"); cmd.arg("."); if size_sorted { cmd.arg("-S"); } cmd.spawn(); #}
消費ビルダー
ビルダーがTを生成する際に、所有権を移動しなければならない場合があります。
つまり終端メソッドが&selfではなくselfを取るときです。
# #![allow(unused_variables)] #fn main() { impl TaskBuilder { /// Name the task-to-be. pub fn named(mut self, name: String) -> TaskBuilder { self.name = Some(name); self } /// Redirect task-local stdout. pub fn stdout(mut self, stdout: Box<io::Write + Send>) -> TaskBuilder { self.stdout = Some(stdout); self } /// Creates and executes a new child task. pub fn spawn<F>(self, f: F) where F: FnOnce() + Send { /* ... */ } } #}
ここで、stdoutを設定するときにはio::Writeを実装する型の所有権が渡されるため、
タスクの生成を行う際にその所有権を移動する必要があります(spawn内)。
ビルダーの終端メソッドが所有権を要求するとき、大きなトレードオフが存在します。
-
もし他のビルダーメソッドが可変借用を受け渡すと、 前述した複雑な設定の場合は上手く動きますが、ワンライナーでの設定は不可能になります。
-
もし他のビルダーメソッドが
selfの所有権を受け渡すと、 ワンライナーはそのまま動きますが、複雑な設定を行う際には不便です。
簡単なものは簡単なまま保つべきですから、
消費ビルダーのビルダーメソッドは全てselfを取り、返すべきです。
このビルダーを使用するコードは次のようになります。
# #![allow(unused_variables)] #fn main() { // One-liners TaskBuilder::new("my_task").spawn(|| { /* ... */ }); // Complex configuration let mut task = TaskBuilder::new(); task = task.named("my_task_2"); // must re-assign to retain ownership if reroute { task = task.stdout(mywriter); } task.spawn(|| { /* ... */ }); #}
所有権がspawnに消費されるまで各メソッド間で次々と受け渡され、ワンライナーは今までどおり動きます。
一方で、複雑な設定は記述量が増えています。各メソッドを呼ぶ際に再束縛が必要になります。
信頼性
関数が引数を検証している (C-VALIDATE)
RustでのAPIは「送るものに関しては厳密に、受け取るものに関しては寛容に」という堅牢性原則には縛られません。
代わりに、Rustコードは可能なかぎり入力の正しさを 検証 すべきです。
この検証は、以下のようにして行うことができます(より推奨されるものの順に並んでいます)。
静的な検証
不正な値を受け付けないよう引数の型を選んでください。
例えば次のようにします。
# #![allow(unused_variables)] #fn main() { fn foo(a: Ascii) { /* ... */ } #}
このようにしてはいけません。
# #![allow(unused_variables)] #fn main() { fn foo(a: u8) { /* ... */ } #}
ここでAsciiはu8の ラッパ であり、最上位ビットがゼロであることを保証します。
型安全なラッパを作る方法はnewtypeパターン(C-NEWTYPE)を参照してください。
静的な検証は型の境界にコストを押し込む(例えば、u8はAsciiに変換しなければ受け付けない)ため、
実行時コストが掛かることは余りありません。 また、実行時ではなくコンパイル中にバグが検出されます。
一方、型を用いて表すことが困難あるいは不可能な特性も存在します。
動的な検証
入力を処理と同時に(あるいは必要ならば事前に)検証します。 動的な検証は静的な検証よりも実装が簡単ですが、いくつかの欠点があります。
- 実行時コスト (処理と検証を同時に行うことができない場合)
- バグの検出が遅れます
- パニックや
Result/Option型による失敗ケースを呼び出し側のコードで処理しなければなりません
debug_assert!による動的な検証
プロダクションビルドにおいて高コストな検証を行わないようにできるかもしれません。
動的な検証のオプトアウト
チェックを行わないバージョンの関数を追加します。
チェックを行わない関数の名前の後ろに_uncheckedと付けたり、rawという名前のモジュールに置かれたりするのが一般的です。
チェックを行わない関数は(1)パフォーマンスがチェックよりも優先される場合 (2)入力が正しいと呼び出し側が確信している場合に使うことができます。
デストラクタが失敗しない (C-DTOR-FAIL)
デストラクタはパニック時にも実行されますが、その際にさらにデストラクタ内でパニックすると プログラムは強制終了します。
デストラクタでパニックする代わりに、Resultを返して失敗を通知するcloseメソッドのような、
失敗を確認することのできる破棄メソッドを追加してください。
ブロックする可能性のあるデストラクタには代替手段を用意する (C-DTOR-BLOCK)
デバッグが難しくなるため、デストラクタでブロックするような操作を行うべきではありません。 ブロックせずに破棄を行える別のメソッドを追加するべきです。
デバッガビリティ
全てのパブリックな型にDebugを実装する (C-DEBUG)
例外が必要なことは稀なはずです。
Debug表現を空にしない (C-DEBUG-NONEMPTY)
Debug表現は空にするべきではありません。概念的に空である値に対しても同様です。
# #![allow(unused_variables)] #fn main() { let empty_str = ""; assert_eq!(format!("{:?}", empty_str), "\"\""); let empty_vec = Vec::<bool>::new(); assert_eq!(format!("{:?}", empty_vec), "[]"); #}
将来性
sealedトレイトを使って下流の実装を適切に防いでいる (C-SEALED)
そのクレート内でのみ実装されることを想定したトレイトについて、 sealedトレイトパターンを用いることでユーザのコードを壊すことなしに変更を加えることが可能になります。
# #![allow(unused_variables)] #fn main() { /// このトレイトはsealされているため、他のクレートで実装を追加することはできません。 pub trait TheTrait: private::Sealed { // メソッド fn ...(); // ユーザが呼ぶべきでないプライベートメソッド #[doc(hidden)] fn ...(); } // 実装 impl TheTrait for usize { /* ... */ } mod private { pub trait Sealed {} // 同じ型に実装 impl Sealed for usize {} } #}
プライベートな空のSealed親トレイトを下流のクレートから参照することはできません。
従って、Sealed(そしてTheTrait)の実装はこのクレート内にのみ存在できます。
トレイトにメソッドを追加することは一般的に破壊的変更となりますが、
sealedトレイトであるTheTraitにメソッドを追加することは破壊的変更になりません。
また、ドキュメントに掲載されていないメソッドの定義も自由に変更することができます。
sealedトレイトからパブリックなメソッドを取り除いたり、 定義を変更したりすることは依然として破壊的変更であることに注意してください。
混乱したユーザがそれらのトレイトを実装しようとすることを防ぐため、 そのトレイトはsealされており、他のクレートから実装されるべきものではないことを ドキュメントに記載しておくべきです。
例
構造体のフィールドを適切にプライベートにする (C-STRUCT-PRIVATE)
構造体のフィールドをパブリックにすることには重大な責任が伴います。 表現を変更することはできなくなり、またユーザはフィールドを自由に弄ることができるため 値のバリデーションや不変条件の検証などができなくなります。
パブリックなフィールドはC言語的な意味あいのsturct、すなわち複合化された受け身のデータ構造には最適ですが、
それ以外ではgetter/setterメソッドを用意しフィールドを隠蔽することを考慮してください。
newtypeを用いて実装詳細を隠蔽している (C-NEWTYPE-HIDE)
newtypeはユーザへの保証を保ちつつ実装詳細を隠蔽するために役立ちます。
例としてこの、イテレータ型を返すmy_transform関数を見てください。
# #![allow(unused_variables)] #fn main() { use std::iter::{Enumerate, Skip}; pub fn my_transform<I: Iterator>(input: I) -> Enumerate<Skip<I>> { input.skip(3).enumerate() } #}
ユーザから見た際にIterator<Item = (usize, T)>のように見えるよう型を隠したいときは、
newtype型を使うことができます。
# #![allow(unused_variables)] #fn main() { use std::iter::{Enumerate, Skip}; pub struct MyTransformResult<I>(Enumerate<Skip<I>>); impl<I: Iterator> Iterator for MyTransformResult<I> { type Item = (usize, I::Item); fn next(&mut self) -> Option<Self::Item> { self.0.next() } } pub fn my_transform<I: Iterator>(input: I) -> MyTransformResult<I> { MyTransformResult(input.skip(3).enumerate()) } #}
これにより宣言が簡単になるだけでなく、ユーザへの保証を小さくすることができます。 ユーザは返されたイテレータがどのように生成されたのか、どのような内部表現になっているのかを知ることができません。 したがって、ユーザのコードを壊すこと無く将来的に内部表現を変更できるようになります。
impl Traitは現在のところunstableですが、将来的にはこれを用いても同じことが達成できるようになります。
# #![allow(unused_variables)] #![feature(conservative_impl_trait)] #fn main() { pub fn my_transform<I: Iterator>(input: I) -> impl Iterator<Item = (usize, I::Item)> { input.skip(3).enumerate() } #}
データ構造にderiveしたトレイトの境界を定義で繰り返さない (C-STRUCT-BOUNDS)
ジェネリックなデータ構造はderiveしたトレイト境界をその定義において繰り返すべきではありません。
derive属性によって実装されたトレイトは、ジェネリック型がそのトレイトを実装している場合のみ実装される
個別のimplブロックに展開されます。
# #![allow(unused_variables)] #fn main() { // 良い例: #[derive(Clone, Debug, PartialEq)] struct Good<T> { /* ... */ } // 悪い例: #[derive(Clone, Debug, PartialEq)] struct Bad<T: Clone + Debug + PartialEq> { /* ... */ } #}
Badのようにderiveしたトレイトを境界として繰り返すのは不要であり、
しかも後方互換性を保つ上で困難となります。
なぜなら、ここでPartialOrdをderiveした場合を考えてみて下さい。
# #![allow(unused_variables)] #fn main() { // 非破壊的変更: #[derive(Clone, Debug, PartialEq, PartialOrd)] struct Good<T> { /* ... */ } // 破壊的変更: #[derive(Clone, Debug, PartialEq, PartialOrd)] struct Bad<T: Clone + Debug + PartialEq + PartialOrd> { /* ... */ } #}
一般的に、データ構造にトレイト境界を追加すると全ての利用箇所において追加の境界を満たす必要が発生するため、
破壊的変更となります。
しかし、derive属性を用いて標準ライブラリのトレイトを実装することは破壊的変更となりません。
以下のトレイトはデータ構造において境界とするべきではありません。
ClonePartialEqPartialOrdDebugDisplayDefaultSerializeDeserializeDeserializeOwned
ReadやWriteのようなderiveできないトレイトの中には、
厳密にはデータ構造によって要求されないグレーゾーンのものが存在します。
これらは型のふるまいを伝える役に立つ可能性がありますが、一方で将来的な拡張性の障害にもなります。
しかし、deriveできるトレイトを境界に追加するよりは問題が少ないでしょう。
例外
データ構造にトレイト境界が必要となる、3つの例外があります。
- データ構造がトレイトの関連型を参照している。
?Sized境界。- データ構造がそのトレイト境界を必要とする
Drop実装を持っている。Rustは現在、Drop実装の境界がデータ構造自身にもすることを要求します。
標準ライブラリでの例
std::borrow::CowはBorrowトレイトの関連型を参照しています。std::boxed::Boxは暗黙のSized境界を除いています。std::io::BufWriterはDrop実装に必要である境界を型に要求します。
必要事項
stableなクレートのパブリックな依存クレートがstableである (C-STABLE)
全てのパブリックな依存クレートがstable(>=1.0.0)とならない限り、クレートをstableにすることはできません。
パブリックな依存性は、依存先に由来する型がそのクレートのパブリックなAPIに使われているものです。
# #![allow(unused_variables)] #fn main() { pub fn do_my_thing(arg: other_crate::TheirThing) { /* ... */ } #}
この関数を含むクレートは、other_crateがstableにならない限りstableになりません。
パブリックな依存制は思わぬ所に潜んでいることがあるため、注意が必要です。
# #![allow(unused_variables)] #fn main() { pub struct Error { private: ErrorImpl, } enum ErrorImpl { Io(io::Error), // ErrorImplはプライベートなので、other_crateがstableでなくても問題ないはず。 Dep(other_crate::Error), } // いや、ここでother_crateをパブリックなAPIの一部にしてしまっている。 impl From<other_crate::Error> for Error { fn from(err: other_crate::Error) -> Self { Error { private: ErrorImpl::Dep(err) } } } #}
クレートとその依存先がpermissiveなライセンスの下にある (C-PERMISSIVE)
Rustプロジェクトによって公開されているソフトウェアはMITとApache 2.0 のデュアルライセンス下にあります。 Rustエコシステムとの最大の協調性が必要なクレートは同じようにするべきです。 その他の選択肢も以下に提示します。
Rustのライセンスに関してはRust FAQで幾分かの言及がなされていますが、 このAPIガイドラインでは詳しい説明は行いません。 これはライセンスに関する解説ではなく、Rustとの協調性に関するガイドラインだからです。
Rustと同等のライセンスをあなたのプロジェクトに適用したい場合、次のように
Cargo.toml中のlicenseフィールドを設定してください。
[package]
name = "..."
version = "..."
authors = ["..."]
license = "MIT/Apache-2.0"
そして、README.mdの最後に以下を追加してください。
## License
Licensed under either of
* Apache License, Version 2.0
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
MIT/Apache-2.0のデュアルライセンスの他に、Rustのクレート開発者によってよく使われるのは MITやBSDのようなpermissiveなライセンスを単体で利用することです。 このやり方でも、小さな制約が加わってしまうだけでRustとの互換性はあります。
Rustとの完全なライセンス互換性が必要なら、Apacheライセンス単体を用いることは推奨されません。 Apacheライセンスはpermissiveではありますが、MITやBSDより大きな制約を課しているため、 デュアルライセンスのRust本体は使うことができるにも関わらず Apacheライセンスのみのソフトウェアを使うことのできない状況が存在するからです。
クレートの依存先のライセンスはクレート自身の配布にも制約を与える可能性があるため、 permissiveなライセンスのクレートはpermissiveなライセンスのクレートにのみ依存するべきです。
外部リンク
- RFC 199 - Ownership naming conventions
- RFC 344 - Naming conventions
- RFC 430 - Naming conventions
- RFC 505 - Doc conventions
- RFC 1574 - Doc conventions
- RFC 1687 - Crate-level documentation
- Elegant Library APIs in Rust
- Rust Design Patterns