パターンとはその言語が抽象化できなかった敗北の歴史である。 しかしどんなに優れた言語であってもあらゆる繰り返しに勝てるわけではない。 人は必ずメタ繰り返しを欲するからだ。 そしてそれはRustも例外ではない。
ここでは、OOPでも知られているパターンよりも、Rustに特有のパターンを思いつく限りまとめてみた。名前は適当。
- crate splitting
- でかいcrateを分割して、見通しを良くする・再コンパイルの分量を削減する・並列コンパイルを可能にする
- 親玉crate(全てにdependする)と殿crate(全てにdependされる)があることが多いので、だいたい束みたいな形になる。
- num, tokio, rustcなど、でかい所はだいたいこれをやっている
- rustcのそれは特に見通しは良くない。単に並列コンパイル用っぽい気がする。
- incrementalが進化してrustcがスレッド並列になったら必要性は薄まるかも
- test module
- テスト関数と、テストにしか使わない関数を、
mod tests {}
に閉じ込める。 - 当該モジュールには普通
#[cfg(test)]
をつける。 - 当該モジュール内で
use super::*;
とすることも多い。テスト対象のモジュールとほぼ同じ見え方になるので。
- テスト関数と、テストにしか使わない関数を、
- newtype
struct NewType(OldType);
- 型システム上区別される別名をつける。 (型システム上同一の別名をつけるときは
type NewType = OldType;
) - Deref inheritanceと併用することもある。 (
OldType
のうち特定の条件を満たしたものをNewType
にしたときなど) PhantomData
が必要なこともある。struct NewType<X>(OldType, PhantomData<fn() -> X>);
とか。
- panic guard
try .. finally
やbegin .. ensure
に相当する処理を、Rustのpanicに対して行う方法。主にunsafeコードで使われる。- 「パニックの有無にかかわらず行う処理」をするための専用の構造体を用意し、Dropを実装する。
- しばしば、この構造体は関数内で定義される。
- パニックしなかった場合はforgetする、という亜種もある。
- callback
- FnOnceクロージャを受け取り、それを必ず呼ぶ。
- 「入る処理」と「出る処理」がペアであることを強制するために使う。
- 「出る処理」をDropで書く場合とは異なり、ネストの出る順は入れ替わらないし、forgetにも耐性がある。
- panic guardと組み合わせれば、panic時も出る処理を強制できる。
- take pattern
- 初期化/未初期化を静的に保証できないケースで
Option<T>
を使う。 - futures::Futureでは「pollが一度でもReadyを返したら、次はpollは呼ばれない」という規約があるが、これはRustの型ではうまく書けない。 なのでfutures関連コードではかなり使われている。
- 初期化/未初期化を静的に保証できないケースで
- extension trait pattern
- 任意オブジェクトにメソッドを生やす、Rubyのオープンクラス的なことをしたいときに使う。
HogeExt
みたいなトレイトを作って、欲しいオブジェクトにだけ実装させる。- 使う側は
use HogeExt
する必要があり、勝手に使われることはない。 - 命名規則がRFCで定められている、実質公認パターン。
- prelude pattern
- ↑のトレイトが多すぎるときに使う。
some_crate::prelude
というモジュールに、これは入れとけというやつを全部突っ込んでおく。- これを使ってると大御所感がある。
- specialization marker
- 特殊化のためのマーカートレイト。特定の条件を満たすときに実装しておくと速くなる。
- 条件を満たさないのに実装するとおかしな挙動になる。
- マーカーじゃない場合もある。
- stdの
FusedIterator
やTrustedLen
,TrustedRandomAccess
がこれ。
- specializer trait
- 特殊化のための内部トレイト。特殊化はトレイト単位でしかできないので、トレイト単位ではない特殊化をするために専用のトレイトを用意する。
- 公開されているトレイトで特殊化をやるとopen specialization (downstream crateが特殊化を定義できてしまう)になってしまうが、専用のトレイトを用意すればclosed specializationにできるという利点もあるかもしれない。
- std内の
SpecExtend
などで使われている。
- companion struct
- メソッドと、その戻り値となる構造体が一対一対応している。構造体は、ある特定のトレイトを実装している以外は何のとりえもない。
- Iteratorなどで使われている。
impl Trait
が安定化されたので、単純な問題に対してはこれで事足りるようになってきた。- しかし、Iteratorのような複雑なケースでは依然としてcompanion struct patternが必要
- dummy token
- 宣言マクロでたまに必要になる。
- こちらの我田引水リンクを参照。
- 必須じゃなくても、わざわざマクロ名を増やしたくないときに使うことがある。 (
mymacro!(expr)
が内部でmymacro!(@foo expr)
を呼ぶみたいな)
- Deref inheritance
- オブジェクト指向が恋しくなったときに使う。
struct Sub { base: Super, .. }
のような構造体を定義する。Sub
がSuper
にDerefするようにする。- オーバーライドもダウンキャストもないが、アップキャストがオブジェクト指向っぽく振る舞う。
- rustcのFnCtxt, TyCtxt, InferCtxtあたりで使われている。
- struct-enum
- enumの共通項を括り出したいときに
struct Foo
がenum FooKind
を含む形にする。 - RustコンパイラのASTとかの定義で頻出。
- failureのドキュメントにも同じパターンが紹介されている。
- enumの共通項を括り出したいときに
- exhaustive field
- publicな構造体だけど、今後フィールドが増えそうなときに使う……かもしれない。
- ダミーのprivate fieldを置けば、外からは今後フィールドが増えそうな感じに見える。
- ただこれだとupdate syntaxが使えなくなるので、隠しフィールドっぽい名前のpublic fieldを置いて
#[doc(hidden)]
をつけるほうが良いかも。
- exhaustive variant
- enumのvariantを今後も増やしたい、というときに使う。
- やりかたは上と同じで、ダミーのvariantを用意して
#[doc(hidden)]
をつける。 ErrorKind
で使われている。
{expr}
(expr)
と{expr}
は挙動が異なる場合がある。- 特に、再借用を抑制する効果があり、
&mut T
から&mut T
を取り出して再代入するような処理をするときに使われる。(単方向連結リストなど) - 詳細はbluss氏のブログを参照。