Rustのデザインパターン
デザインパターンとは、何度も出てくる問題に対する、実績のある解き方のことです。パターンは、インストールするライブラリではありません。どちらかというとレシピに近いものです。「こういう問題があるときは、コードをこんな形にしよう」というものです。
Java や C# のパターンの多くは、Rust では違う形になったり、そもそも必要なかったりします。Rust には、強力な型システム、列挙型(enum)、トレイト、所有権をもとにした、Rust ならではの定番パターンがあります。そのほとんどに共通する大きな考え方は、ミスはコンパイラに見つけてもらうことです。そうすれば、ミスがユーザーのところまで届くことはありません。以下の各セクションでは、まず問題を示し、それからその問題を解決するパターンを紹介します。
ニュータイプ:意味が違えば型も違う
Section titled “ニュータイプ:意味が違えば型も違う”問題。ただの f64 は、何でも表せてしまいます。メートル、フィート、秒、ユーロ……。メートル単位の f64 を受け取る関数にフィート単位の値を渡しても、コンパイラは助けてくれません。どちらもただの f64 だからです。(こうした単位の取り違えで、NASA が1999年に火星探査機マーズ・クライメイト・オービターを失ったのは有名な話です。)
パターン。値を、フィールドが1つだけの小さな構造体で包みます。これで Meters と Feet は別々の型になり、Rust はこの2つを取り違えることを許しません。
#[derive(Debug, Clone, Copy)]struct Meters(f64);
#[derive(Debug, Clone, Copy)]struct Feet(f64);
impl Feet { fn to_meters(self) -> Meters { Meters(self.0 * 0.3048) }}
fn safe_altitude(height: Meters) -> bool { height.0 >= 300.0}
fn main() { let tower = Meters(330.0); let plane = Feet(900.0);
println!("タワーは安全? {}", safe_altitude(tower)); println!("900フィートは{:.1}メートル", plane.to_meters().0); println!("飛行機は安全? {}", safe_altitude(plane.to_meters()));}実行結果:
タワーは安全? true900フィートは274.3メートル飛行機は安全? false変換を忘れて safe_altitude(plane) と書くと、プログラムはコンパイルすらできません。
error[E0308]: mismatched types --> src/main.rs:13:34 |13 | println!("{}", safe_altitude(plane)); | ------------- ^^^^^ expected `Meters`, found `Feet` | | | arguments to this function are incorrect「型が合わない。Meters を期待したのに Feet が渡された」というエラーです。
ニュータイプは、実行時には何のコストもかかりません。メモリ上では、Meters はただの f64 そのものです。ID(UserId(u32) と OrderId(u32))、単位、検証済みの文字列などに使いましょう。孤児ルール(orphan rule)を回避するのにも使えます(高度なトレイトを見てください)。
ビルダー:たくさんのオプションから、わかりやすい1つの結果を
Section titled “ビルダー:たくさんのオプションから、わかりやすい1つの結果を”問題。フィールドがたくさんあって、そのほとんどが省略可能な構造体があります。Email::new("[email protected]", "Hi", "", vec![], false) のようなコンストラクタは読みにくいですよね。どの false が何を意味するのでしょう? それに、アドレスがなかったり、間違っていたりしたらどうなるでしょう?
パターン。別のビルダー構造体を用意して、名前の付いた小さなメソッドで設定を集めます。どのメソッドもビルダーを返すので、呼び出しをチェーン(連結)できます。最後の build() がすべてをチェックして、Result を返します。
struct Email { to: String, subject: String, body: String, urgent: bool,}
#[derive(Default)]struct EmailBuilder { to: Option<String>, subject: Option<String>, body: String, urgent: bool,}
impl EmailBuilder { fn new() -> Self { EmailBuilder::default() }
fn to(mut self, address: &str) -> Self { self.to = Some(address.to_string()); self }
fn subject(mut self, subject: &str) -> Self { self.subject = Some(subject.to_string()); self }
fn body(mut self, text: &str) -> Self { self.body = text.to_string(); self }
fn urgent(mut self) -> Self { self.urgent = true; self }
fn build(self) -> Result<Email, String> { let to = self.to.ok_or("メールには宛先が必要です")?; if !to.contains('@') { return Err(format!("'{}' はメールアドレスではありません", to)); } let subject = self.subject.unwrap_or_else(|| String::from("(件名なし)")); Ok(Email { to, subject, body: self.body, urgent: self.urgent }) }}
fn show(result: Result<Email, String>) { match result { Ok(e) => println!("準備完了: to={} subject={} body={:?} urgent={}", e.to, e.subject, e.body, e.urgent), Err(msg) => println!("エラー: {}", msg), }}
fn main() { let email = EmailBuilder::new() .subject("ランチどう?") .body("12時にピザはどう?") .urgent() .build(); show(email);
show(EmailBuilder::new().subject("こんにちは").build()); show(EmailBuilder::new().to("ana.example.com").build());}実行結果:
準備完了: [email protected] subject=ランチどう? body="12時にピザはどう?" urgent=true準備完了: [email protected] subject=(件名なし) body="" urgent=falseエラー: メールには宛先が必要ですエラー: 'ana.example.com' はメールアドレスではありませんそれぞれのセッターは mut self を受け取り(ビルダーの所有権を受け取ります)、フィールドを1つ変えて、ビルダーを返します。これでチェーンができるのです。呼び出す側は気になるものだけを設定すればよく、メソッド名が値の意味を説明してくれます。そして、ルールをチェックするのは build() の1か所だけです。その中では、ok_or が宛先のない None を Err に変え、? がそのエラーを早めに返します。ビルダーはたくさんのクレートで使われています。たとえば std::thread::Builder や std::process::Command です(環境変数とプロセスを見てください)。
Default と構造体更新記法:手軽な選択肢
Section titled “Default と構造体更新記法:手軽な選択肢”問題。ビルダーはコードがたくさん必要です。チェックすべきルールがなく、ただ妥当な初期値があればいいだけの構造体には、大げさすぎます。
パターン。よい初期値で Default を実装し、構造体更新記法 .. を使って、気になるフィールドだけを変えます。
#[derive(Debug, Clone)]struct ServerConfig { host: String, port: u16, workers: u32, verbose: bool,}
impl Default for ServerConfig { fn default() -> Self { ServerConfig { host: String::from("127.0.0.1"), port: 8080, workers: 4, verbose: false, } }}
fn main() { let dev = ServerConfig { verbose: true, ..Default::default() }; let prod = ServerConfig { host: String::from("0.0.0.0"), workers: 16, ..ServerConfig::default() }; // デフォルトではなく、別の設定をもとにする: let staging = ServerConfig { port: 9090, ..prod.clone() };
println!("{:?}", dev); println!("{:?}", prod); println!("{:?}", staging);}実行結果:
ServerConfig { host: "127.0.0.1", port: 8080, workers: 4, verbose: true }ServerConfig { host: "0.0.0.0", port: 8080, workers: 16, verbose: false }ServerConfig { host: "0.0.0.0", port: 9090, workers: 16, verbose: false }..something は「書かなかったフィールドは全部 something から持ってくる」という意味です。なぜ prod.clone() なのでしょうか? 構造体更新記法は、残りのフィールドをムーブします。そして host は String です。clone しないと prod は一部がムーブされた状態になり、あとで表示できなくなってしまいます。目安としては、どんな値の組み合わせでも問題ないなら Default と .. を、無効な組み合わせがあるならビルダーを使いましょう。
タイプステート:コンパイル時にチェックされる状態機械
Section titled “タイプステート:コンパイル時にチェックされる状態機械”問題。順番どおりに起こらなければならないことがあります。Web のリクエストは、URL を設定してからでないと送れません。ふつうの構造体なら、実行時にチェックすることになります(「URL は設定されている? されていなければエラーを返す」)。そして、すべての呼び出し側がそのエラーをきちんと処理してくれることを祈るしかありません。
パターン。状態を型の一部にします。Request<NoUrl> には、そもそも send メソッドがありません。URL を設定したときだけ Request<HasUrl> に変わり、そこで初めて send が使えるようになります。
// 2つの状態。データは持たない(か、その状態に必要なものだけを持つ)。struct NoUrl;struct HasUrl(String);
struct Request<U> { url: U, headers: Vec<(String, String)>,}
impl Request<NoUrl> { fn new() -> Self { Request { url: NoUrl, headers: Vec::new() } }
// 型を変える:Request<NoUrl> -> Request<HasUrl> fn url(self, url: &str) -> Request<HasUrl> { Request { url: HasUrl(url.to_string()), headers: self.headers } }}
// どの状態でも使えるimpl<U> Request<U> { fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }}
// URL が設定されてからしか使えないimpl Request<HasUrl> { fn send(self) -> String { format!("GET {}(ヘッダー{}個)", self.url.0, self.headers.len()) }}
fn main() { let response = Request::new() .header("Accept", "text/html") .url("https://example.com") .header("User-Agent", "rust-tutorial") .send();
println!("{}", response);}実行結果:
GET https://example.com(ヘッダー2個)では、URL なしで送ってみましょう。Request::new().send() と書くと、コンパイラが止めてくれます。
error[E0599]: no method named `send` found for struct `Request<NoUrl>` in the current scope --> src/main.rs:22:35 |22 | let response = Request::new().send(); | ^^^^ method not found in `Request<NoUrl>` | = note: the method was found for `Request<HasUrl>`「Request<NoUrl> には send というメソッドがない。Request<HasUrl> にはある」というエラーです。
コンパイルが通るプログラムには、このバグは存在しえません。実行時のチェックも、余計なコストもありません。NoUrl のサイズは0バイトです。url() は self を値で受け取るので、古い Request<NoUrl> は使い切られます。だから、うっかり「設定前」の状態を使い続けることもできません。これはジェネリクスの巧みな使い方です。Playground で少し書き換えてみて、どう動くのかを体感してみる価値があります。
RAII と Drop ガード:忘れようのない後片付け
Section titled “RAII と Drop ガード:忘れようのない後片付け”問題。リソースには後片付けが必要です。ファイルを閉じる、ロックを解除する、一時フォルダを消す、「完了」と表示する。後片付けが「忘れずに呼ばなければならない関数」だと、誰かが必ず忘れます。早めの return やエラーのときは特にそうです。
パターン。後片付けを Drop に書きます。Rust は、値がスコープを抜けるときに自動で drop を呼びます。ふつうに終わったときも、早めに return したときも、? のときも、パニックのときでさえ、どの道筋で抜けても呼ばれます。これを RAII(Resource Acquisition Is Initialization:リソースの取得と、値を所有することが同じ一歩になる)と呼びます。後片付けだけが仕事の値は、ガードと呼ばれます。
struct Guard { name: String,}
impl Guard { fn new(name: &str) -> Guard { println!("{}を開始", name); Guard { name: name.to_string() } }}
impl Drop for Guard { fn drop(&mut self) { println!("{}を後片付け", self.name); }}
fn risky(fail: bool) -> Result<(), String> { let _guard = Guard::new("危険な作業"); if fail { return Err(String::from("問題が起きました")); // それでもガードは後片付けする } println!("作業完了"); Ok(())}
fn main() { { let _block = Guard::new("ブロック"); println!("ブロックの中"); } // _block はここで破棄される
println!("{:?}", risky(false)); println!("{:?}", risky(true));}実行結果:
ブロックを開始ブロックの中ブロックを後片付け危険な作業を開始作業完了危険な作業を後片付けOk(())危険な作業を開始危険な作業を後片付けErr("問題が起きました")このパターンは、実はもういつも使っています。File は破棄されるときに自分で閉じます。MutexGuard(mutex.lock() が返すもの。並行処理を見てください)は、破棄されるときにミューテックスのロックを解除します。Box は、破棄されるときにメモリを解放します(スマートポインタ)。Rust のコードに「close」や「free」の呼び出しがほとんど出てこないのは、このためです。
From と Into:引数を受け取りやすくする
Section titled “From と Into:引数を受け取りやすくする”問題。関数が String を受け取るので、&str を持っている呼び出し側は、毎回 String::from("Ana") や "Ana".to_string() と書かなければなりません。あるいは、関数は UserId を受け取るのに、呼び出し側が持っているのはただの u32 だったりします。小さなことですが、あちこちでコードがごちゃごちゃします。
パターン。自分の型に From を実装し、関数では impl Into<T> を受け取るようにします。そうすれば、呼び出し側は変換できるものなら何でも渡せます。
#[derive(Debug)]struct UserId(u32);
impl From<u32> for UserId { fn from(n: u32) -> Self { UserId(n) }}
#[derive(Debug)]struct User { id: UserId, name: String,}
impl User { fn new(id: impl Into<UserId>, name: impl Into<String>) -> Self { User { id: id.into(), name: name.into() } }}
fn main() { let ana = User::new(1, "Ana"); // u32 と &str let ben = User::new(UserId(2), String::from("Ben")); // 最初から正しい型
println!("{:?}", ana); println!("{:?}", ben);}実行結果:
User { id: UserId(1), name: "Ana" }User { id: UserId(2), name: "Ben" }実装するのは From にして、Into を直接実装することはしません。Into はおまけで付いてくるからです。同じ仕組みがエラー処理も支えています。? はエラーに対して From::from を呼ぶので、From<io::Error> for MyError を実装しておけば、? がエラーを変換してくれます(独自のエラー型を見てください)。これは失敗しない変換に使いましょう。失敗するかもしれない変換には、Result を返す TryFrom を使います。
enum の状態機械と、ありえない状態
Section titled “enum の状態機械と、ありえない状態”問題。注文は「カートの中」から「支払い済み」、そして「発送済み」へと進みます。最初に書いてみると、よくこんな形になります。
struct Order { paid: bool, shipped: bool, tracking_number: Option<String>,}でもこれだと、おかしな状態を作れてしまいます。paid: false なのに shipped: true だったり、まだ発送していない注文に追跡番号があったり。どの関数でも、こうしたありえない組み合わせをチェックしなければなりません。
パターン。ありえない状態を表現できないようにします。状態ごとに1つのバリアントを持つ enum を使い、各バリアントにはその状態が本当に持つデータだけを持たせます。状態の遷移は、古い状態を受け取って新しい状態を返すメソッドにします。
#[derive(Debug)]enum Order { Cart { items: Vec<String> }, Paid { items: Vec<String>, amount: u32 }, Shipped { tracking: String }, Cancelled,}
impl Order { fn pay(self, amount: u32) -> Order { match self { Order::Cart { items } => Order::Paid { items, amount }, other => other, // 支払いできるのはカートだけ。それ以外はそのまま } }
fn ship(self, tracking: &str) -> Order { match self { Order::Paid { .. } => Order::Shipped { tracking: tracking.to_string() }, other => other, } }
fn cancel(self) -> Order { match self { Order::Shipped { .. } => self, // キャンセルするには遅すぎる _ => Order::Cancelled, } }
fn status(&self) -> String { match self { Order::Cart { items } => format!("カート(商品{}個)", items.len()), Order::Paid { items, amount } => format!("支払い済み:金額{}(商品{}個)", amount, items.len()), Order::Shipped { tracking } => format!("発送済み、追跡番号 {}", tracking), Order::Cancelled => String::from("キャンセル済み"), } }}
fn main() { let order = Order::Cart { items: vec![String::from("本"), String::from("ペン")] }; println!("{}", order.status());
let order = order.ship("XYZ"); // まだ支払っていない:何も起きない println!("{}", order.status());
let order = order.pay(25).ship("TRACK-42"); println!("{}", order.status());
let order = order.cancel(); // もう発送済み println!("{}", order.status());
let other = Order::Cart { items: vec![] }.cancel(); println!("{}", other.status());}実行結果:
カート(商品2個)カート(商品2個)発送済み、追跡番号 TRACK-42発送済み、追跡番号 TRACK-42キャンセル済み発送済みの注文に追跡番号がないということはありえません。Shipped バリアントは必ず追跡番号を持っているからです。カートが追跡番号を持つこともありえません。そのためのフィールドがないからです。そして、match はすべてのバリアントを網羅しなければならないので、新しい状態(たとえば Refunded)を追加すると、直すべき場所をすべてコンパイラが教えてくれます。
enum か、トレイトオブジェクトか? オブジェクト指向の言語では、「State」インターフェースを用意して、状態ごとに1つのクラスを作ることがよくあります。Rust でもトレイトオブジェクト(Box<dyn State>)を使えば同じことができ、Rust Book にもそのやり方が載っています。選び方は次のとおりです。
- enum:すべての状態が最初からわかっている場合。
matchの網羅性チェックが効き、ロジックが1か所にまとまります。Rust ではふつうこちらを選びます。 - トレイトオブジェクト:ほかの人(やほかのクレート)が、あなたのコードを変えずに新しい状態を追加する必要がある場合。
- タイプステート(前のセクション):呼び出す順番の間違いを、実行時に無視するのではなく、コンパイルエラーにしたい場合。より厳格ですが、状態がコンパイル時にわかっている必要があります。
よくある間違い
Section titled “よくある間違い”let _guard = guard ではなく let _ = guard と書く。ほとんど同じに見えますが、動きが違います。アンダースコアで始まる名前(_guard など)は、値をスコープの終わりまで生かしておきます。ただの _ は変数ですらないので、値はすぐに破棄されます。
struct Guard;
impl Drop for Guard { fn drop(&mut self) { println!("ガードが破棄された"); }}
fn main() { let _ = Guard; println!("`let _` で作業中");
let _g = Guard; println!("`let _g` で作業中");}実行結果:
ガードが破棄された`let _` で作業中`let _g` で作業中ガードが破棄されたlet _ だと、後片付けが作業の前に走ってしまいました。ロックやタイマーにとっては、まさに最悪の動きです。ガードには、必ず名前の付いた変数を使いましょう。
パターンがあるから、というだけで使う。フィールドが2つしかない構造体にビルダーを作ったり、誤用されることのないものにタイプステートを使ったりしても、コードが増えるだけで何も安全になりません。まずはシンプルに始めましょう。パターンを使うのは、そのパターンが解決する問題を実際に感じたときです。
- ニュータイプ(
struct Meters(f64))は、同じデータに違う型を与えます。取り違えはコンパイラが見つけてくれて、実行時のコストはかかりません。 - ビルダーは、読みやすいチェーン形式の設定と、検証して
Resultを返す1つのbuild()を提供します。チェックが何も要らないなら、Default と..のほうが手軽です。 - タイプステートは状態を型に入れます(
Request<NoUrl>とRequest<HasUrl>)。だから、間違った順番で呼び出すとコンパイルが通りません。 - RAII / Drop ガードは、スコープをどう抜けても、後片付けを自動で実行します。
- From/Into を使うと、関数は
impl Into<T>でいろいろな型の入力を受け取れます。 - enum で状態機械を表し、ありえない状態を表現できないようにします。トレイトオブジェクトを使うのは、ほかの人が状態を追加する必要があるときだけにしましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. ニュータイプ struct Meters(f64) を使うと、ただの f64 と比べて何がうれしいですか?
2. このページのビルダーパターンで、build() は何を返しますか?
3. タイプステートの例で、Request::new() に対して send() を呼べないのはなぜですか?
4. Drop ガードの後片付けが実行されるのはいつですか?
5. let _ = Guard; はどうなりますか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- Rust Design Patterns(英語):Rust のイディオム、パターン、アンチパターンをまとめた、コミュニティによる無料の本です。ニュータイプ、ビルダー、RAII ガードも載っています。
- オブジェクト指向デザインパターンを実装する — The Rust Programming Language 日本語版:State パターンをトレイトオブジェクトで実装し、それから状態を型として表す形に書き直しています。
- Rust API Guidelines(英語):使いやすく Rust らしい API を設計するための公式のアドバイス(変換、ビルダー、命名など)。
- RAII — Rust By Example 日本語版:所有権と
Dropによって、後片付けが自動で行われる仕組み。 - The Typestate Pattern in Rust — Cliff L. Biffle(英語):タイプステートについての、より深く、とても読みやすいガイド。例もたくさんあります。