はじめに
参加
本書への寄稿にご興味のある方は、contribution guidelinesをご覧ください。
ニュース
- 2024-03-17:このリンク](https://rust-unofficial.github.io/patterns/rust-design-patterns.pdf)からPDF形式でダウンロードできるようになりました。
デザインパターン
ソフトウェア開発では、それがどのような環境で発生したかに関係なく、共通点を持つ問題に出くわすことがよくあります。目の前の課題を解決するためには実装上の詳細が重要ですが、私たちはこのような詳細から抽出することで、一般的に適用可能な共通プラクティスを見出すことが可能です。
デザインパターンとは、エンジニアリングで繰り返し発生する問題に対する、再利用可能でテスト済みの解決策を集めたものです。デザインパターンは、ソフトウェアをよりモジュール化し、保守しやすく、拡張しやすいものにします。さらに、これらのパターンは開発者に共通言語を提供するため、チームが問題解決する際の効果的なコミュニケーションツールとなります。
Rustのデザインパターン
Rustはオブジェクト指向ではありません。また、関数型プログラミングの要素、強力な型システム、借用チェッカーなど、Rustのすべての特徴の組み合わせがRustをユニークなものにしています。このため、Rust のデザインパターンは他の伝統的なオブジェクト指向プログラミング言語とは異なります。これが私たちがこの本を書くことにした理由です。ご愛読いただければ幸いです!本書は大きく3つの章に分かれています:
- イディオム:コーディングの際に従うべきガイドライン。コミュニティの社会的規範です。正当な理由がある場合のみ破るべきです。
- デザインパターン:コーディング時によくある問題を解決するための手法。
- アンチパターン:コーディング時によくある問題を解決するための手法。しかし、デザイン・パターンには利点がある一方で、アンチ・パターンはより多くの問題を引き起こします。
翻訳
私たちはmdbook-i18n-helperを利用しています。翻訳の追加と更新の方法はそのリポジトリを参照してください。
外部の翻訳
翻訳を追加したい場合は、メインリポジトリ にissueを開いてください。
イディオム
イディオムは、一般的に使われているスタイル、ガイドライン、パターンです。慣用的なコードを書くことで、他の開発者は何が起こっているのかをよりよく理解することができます。
結局のところ、コンピュータにとって重要なものは、コンパイラが生成したマシンコードのみです。むしろ、ソースコードは主に開発者にとって有益なものです。せっかく、私たちには抽象化レイヤーがあるのですから、もっと読みやすくしたらどうでしょう?
KISSの原則 :「シンプルにしておけ!この間抜け」を思い出してください。この原則は、「ほとんどのシステムは、複雑にするよりもシンプルにしたほうが最もうまく機能する。したがって、シンプルであることが設計における重要な目標であるべきであり、不必要な複雑さは避けるべきである」と主張しています。
コードは人間が理解するものであって、コンピューターが理解するものではありません。
引数には借用型(borrowed types)を使用する
説明
関数の引数にどの型を使うかを決めるときに、参照外し型強制(deref coercion)の対象を使うことで、コードの柔軟性を高めることができます。この方法により、関数はより多くの型を入力として受け入れられるようになります。
これはスライス可能な型やファットポインタ型に限ったことではありません。実際、所有型の借用よりも 借用型 を常に使用することをお勧めします。例えば、 &String
よりも &str
の方が、 &Vec<T>
よりも &[T]
の方が、 &Box<T>
よりも &T
の方が優れています。
借用型を使えば、所有型がすでに間接的なレイヤーを提供しているような場合に、そのインスタンスに対する多重の間接的なレイヤーを避けることができます。例えば String
には間接的なレイヤーがあるので、 &String
は2つの間接的なレイヤーを持つことになってしまいます。 代わりに &str
を使用し、関数呼び出し毎に &String
を &str
に型強制することでこれを避けることができます。
例
この例では、関数の引数に &String
を使用した場合に対して、 &str
を使用した場合との違いを説明します。なお、ここで示すものは Vec<T>
に対しての &[T]
や、 &Box<T>
に対しての &T
、などあっても同様のことが言えます。
例として、ある単語が連続した3つの母音を含むかどうかを調べたい場合を考えてみましょう。調べるにあたって、対象の文字列を所有する必要はありません。私たちは参照を使うでしょう。
コードは次のようになります:
fn three_vowels(word: &String) -> bool { let mut vowel_count = 0; for c in word.chars() { match c { 'a' | 'e' | 'i' | 'o' | 'u' => { vowel_count += 1; if vowel_count >= 3 { return true; } } _ => vowel_count = 0, } } false } fn main() { let ferris = "Ferris".to_string(); let curious = "Curious".to_string(); println!("{}: {}", ferris, three_vowels(&ferris)); println!("{}: {}", curious, three_vowels(&curious)); // これはうまくいくが、次の2行は失敗する: // println!("Ferris: {}", three_vowels("Ferris")); // println!("Curious: {}", three_vowels("Curious")); }
これは &String
型をパラメータとして渡しているため、問題なく動作します。最後の2行のコメントを削除すると、この例は失敗します。これは &str
型が &String
型に型強制されないからです。この問題を、単に引数の型を変更することで修正できます。
例えば、関数宣言を次のように変更します:
fn three_vowels(word: &str) -> bool {
すると、上述の例のどちらのバージョンもコンパイルされ、同じ出力が表示されます。
Ferris: false
Curious: true
でも待ってください!この話にはまだ続きがあります。もしかしたら、あなたは「 &'static str
を(上述の例での "Ferris"
で行ったように)入力に使うことは絶対ない・・・どうでもいいかな」と思っているかもしれません。または、この特別な例を無視しても、&str
を使用する方が&'static str
を使用するよりも柔軟性があることに気づくかもしれません。
では、誰かが私たちに文章を与えたとして、その中に3つの母音が連続する単語があるかどうかを調べる例を考えてみましょう。この場合、すでに定義した関数を利用すべきで、単に文中の単語をこの関数に与えるべきでしょう。
この例は次のようなものです:
fn three_vowels(word: &str) -> bool { let mut vowel_count = 0; for c in word.chars() { match c { 'a' | 'e' | 'i' | 'o' | 'u' => { vowel_count += 1; if vowel_count >= 3 { return true; } } _ => vowel_count = 0, } } false } fn main() { let sentence_string = "Once upon a time, there was a friendly curious crab named Ferris".to_string(); for word in sentence_string.split(' ') { if three_vowels(word) { println!("{word} has three consecutive vowels!"); } } }
引数を &str
として宣言した関数を使用してこの例を実行すると、次のようになるでしょう
curious has three consecutive vowels!
しかし私たちが関数の引数型を &String
として宣言していた場合には、この例は実行できません。これは、文字列スライスが &str
であり、&String
ではないためです。 &str
は &String
に変換するためにはアロケーションを必要とし、暗黙期には変換されません。対して String
は &str
に安価かつ暗黙的に変換されます。
See also
- 型強制(Type coercions)に関するRust言語リファレンス
String
と&str
の扱い方に関する議論は、Herman J. Radtke III の このブログのシリーズ(2015) を見てください
`format! による文字列の連結
説明
String
の push
メソッドや push_str
メソッドを使用したり、 +
演算子を使用したりすることで、文字列を構築することは可能です。 しかしformat!
を使用した方がより便利なことがあります。特にリテラル文字列と非リテラル文字列が混在している場合がそうです。
例
#![allow(unused)] fn main() { fn say_hello(name: &str) -> String { // 以下のように result 文字列を構築することができます。 // let mut result = "Hello ".to_owned(); // result.push_str(name); // result.push('!'); // result // しかし format! を使うほうがよいでしょう。 format!("Hello {name}!") } }
長所
format!
を使う方法が、通常、文字列を組み合わせる最も簡潔で読みやすい方法です。
短所
通常、文字列を結合する最も効率的な方法とは言えません。通常、ミュータブルな文字列への push
の繰り返しが最も効率的です(特に、その文字列があらかじめ期待されるサイズに割り当てられている場合)。
コンストラクタ
説明
Rustには、言語の構成要素としてコンストラクタというものを持っていません。 代わりに [関連関数][associated function] の new
を使ってオブジェクトを生成するのが一般的です:
#![allow(unused)] fn main() { /// Time in seconds. /// /// # Example /// /// ``` /// let s = Second::new(42); /// assert_eq!(42, s.value()); /// ``` pub struct Second { value: u64, } impl Second { // Constructs a new instance of [`Second`]. // Note this is an associated function - no self. pub fn new(value: u64) -> Self { Self { value } } /// Returns the value in seconds. pub fn value(&self) -> u64 { self.value } } }
デフォルトコンストラクタ
Rust は [Default
][std-default] トレイトとして、デフォルトコンストラクタをサポートしています:
#![allow(unused)] fn main() { /// 秒単位の時間。 /// /// # 例 /// /// ``` /// let s = Second::default(); /// assert_eq!(0, s.value()); /// ``` pub struct Second { value: u64, } impl Second { /// Returns the value in seconds. pub fn value(&self) -> u64 { self.value } } impl Default for Second { fn default() -> Self { Self { value: 0 } } } }
すべてのフィールドのすべての型が Default
を実装していれば、 Default
も派生させることができます。下記の Second
で行っているように:
#![allow(unused)] fn main() { /// 秒単位の時間。 /// /// # 例 /// /// ``` /// let s = Second::default(); /// assert_eq!(0, s.value()); /// ``` #[derive(Default)] pub struct Second { value: u64, } impl Second { /// Returns the value in seconds. pub fn value(&self) -> u64 { self.value } } }
注意: 型が Default
と 空の new
コンストラクタの両方を実装することは一般的であり、期待されています。new
は Rust におけるコンストラクタの慣習であり、ユーザはその存在を期待しています。基本的なコンストラクタが引数を取らないことが合理的であれば、たとえ機能的には default と同じであるとしても、そのように実装すべきです。
ヒント: Default
を実装または派生させることの利点は、あなたの型が Default
の実装が必要な場所で使用できるようになることです。特に顕著なのは、 [標準ライブラリの *or_default
関数][std-or-default] です。
See also
-
Default
トレイトのより詳細な記述として default idiom。 -
構成が多岐に渡るオブジェクトの構築のための builder pattern。
-
Default
とnew
の両方を実装することについて APIガイドライン/C-COMMON-TRAITS。
Default
トレイト
説明
Rustの多くの型には コンストラクタ があります。しかし、これはその型 固有 のものです。Rustは「 new()
メソッドを持つものすべて」を抽象化することはできません。これを可能にするために、Default
特性が考案され、コンテナや他のジェネリック型と一緒に使用できます(例えば、Option::unwrap_or_default()
を参照してください)。注目すべきは、いくつかのコンテナは、適用可能な場合、すでにこれを実装していることです。
Cow
、Box
、Arc
のような1要素のコンテナが、その内包する型が Default
であるなら、 Default
を実装する、というだけではありません。すべてのフィールドが Default
を実装している構造体に対して #[derive(Default)]
が自動導出可能です。より多くの型が Default
を実装すればするほどより便利になります。
コンストラクタは複数の引数を取ることができる一方、 default()
メソッドではできません。またコンストラクタは異なる名称のものを複数定義することもできますが、 Default
の実装は1つの型につき1つだけです。
例
use std::{path::PathBuf, time::Duration}; // ここでは単に Default を自動導出できることに注意。 #[derive(Default, Debug, PartialEq)] struct MyConfiguration { // Option のデフォルトは None output: Option<PathBuf>, // Vec のデフォルトは空のベクトル search_path: Vec<PathBuf>, // Duration のデフォルトはゼロ時間 timeout: Duration, // bool のデフォルトは false check: bool, } impl MyConfiguration { // ここにセッターを追加する } fn main() { // デフォルト値で新しいインスタンスを作成する let mut conf = MyConfiguration::default(); // ここでconfを使って何かをする conf.check = true; println!("conf = {conf:#?}"); // デフォルト値の上に部分的な初期化、同値のインスタンスを作成 let conf1 = MyConfiguration { check: true, ..Default::default() }; assert_eq!(conf, conf1); }
See also
- constructor イディオムは、インスタンスを生成する別の方法だが、そのインスタンスは「デフォルト」かそうでないかを問わない。
Default
のドキュメント(実装しているもののリストは下にスクロールしてください)。Option::unwrap_or_default()
derive(new)
コレクションはスマートポインタ
説明
コレクションに Deref
トレイトを使うことで、そのデータに対し所有や借用のビューを提供するスマートポインタのようにコレクションを扱えます。
例
use std::ops::Deref;
struct Vec<T> {
data: RawVec<T>,
//..
}
impl<T> Deref for Vec<T> {
type Target = [T];
fn deref(&self) -> &[T] {
//..
}
}
Vec<T>
はT
の所有のコレクションであり、対して、スライス(&[T]
)は T
の借用のコレクションです。Vec
に Deref
を実装することで、 &Vec<T>
から &[T]
への暗黙的な参照外しが可能になるとともに、自動参照外しの検索対象に組み込まれます。Vec
に実装されることが期待されるほとんどのメソッドは、代わりにスライスのそれとして実装されます。
String
と &str
もまた同様の関係にあります。
動機形成
所有権と借用はRust言語の重要な側面です。データ構造は、よりよいユーザエクスペリエンスのため、これらのセマンティクスに則る必要があります。データを所有するデータ構造を実装する場合、そのデータへの借用のビューを提供することで、よりフレキシブルなAPIを提供することが可能になります。
長所
ほとんどのメソッドが借用の型に対して実装可能です。それらメソッドが参照外しにより、所有の型に対しても暗黙的に利用可能になります。
データの借用を行うか、所有権を取るか、利用者が選択できるようにしましょう。
短所
参照外しを通してのみ利用可能なメソッドとトレイトは、境界チェックの際に考慮されません。そのため、このパターンを使用したデータ構造でのジェネリックプログラミングは複雑になる可能性があります (トレイト Borrow
や AsRef
など参照)。
議論
スマート・ポインタとコレクションは相似です。スマートポインタは単一のオブジェクトを指すのに対し、コレクションは多数のオブジェクトを指します。型システムの観点からは、両者にほとんど違いはありません。各データへアクセスする唯一の方法がコレクション経由であるなら、コレクションはそれらデータを所有しており、またそのコレクションはデータを削除する責任を持ちます(共有された所有権の場合でも、何らかの借用のビューは適切な場合があります)。コレクションがそのデータを所有する場合に、そのデータへの借用のビューを提供し複数回参照可能にすることは、一般に有用です。
ほとんどのスマートポインタ(例えば Foo<T>
)は Deref<Target=T>
を実装します。しかしコレクションは通常、カスタム型に参照外しされます。 [T]
と str
にはいくつかの言語サポートがありますが、一般的なケースでは、その必要はありません。Foo<T>
が Deref<Target=Bar<T>>
を実装してかまいません。ここで Bar
は動的なサイズの型であり、&Bar<T>
は Foo<T>
のデータの借用のビューです。
一般的に、順序付きコレクションは Range
に対して Index
を実装し、スライス構文を提供します。その対象は借用のビューになります。
See also
デストラクタでのファイナライズ
説明
Rust には finally
ブロック (関数の終了の仕方を問わずに実行されるコード) に相当するものがありません。その代わりに、オブジェクトのデストラクタを使用して、終了前に実行しなければならないコードを実行させることができます。
例
fn bar() -> Result<(), ()> {
// これらは関数内部で定義する必要はない。
struct Foo;
// Foo のデストラクタを実装する。
impl Drop for Foo {
fn drop(&mut self) {
println!("exit");
}
}
// _exit のデストラクタは、関数`bar`が終了しても実行される。
let _exit = Foo;
// 演算子 `?` による暗黙的なリターン 。
baz()?;
// 通常のリターン。
Ok(())
}
動機形成
関数に複数のリターン・ポイントがある場合、関数終了時にコードを実行するのは難しく、繰り返し的になります(またそれゆえにバグの原因になります)。リターンがマクロによって暗黙的に行われる場合、特にそうなります。よくあるケースが ?
演算子で、これは結果が Err
なら戻り、Ok
なら続行するものです。 ?
は例外処理のメカニズムとして使用されますが、(finally
を持つ) Javaとは異なり、通常と例外の両方のケースで実行されるコードを実装する方法はありません。また、パニックが起きると関数は早期に終了します。
長所
デストラクタ内のコードは (ほぼ) 常に実行されます。パニックや早期リターンなどにも対処できます。
短所
デストラクタの実行は保証されていません。例えば関数の無限ループがある場合や、関数の実行が終了前にクラッシュした場合などです。また、すでにパニックになっているスレッドでパニックが発生した場合にも、デストラクタは実行されません。したがって、絶対的かつ本質的にファイナライズを行うファイナライザとしては、デストラクタを頼ることはできません。
このパターンは、気づきにくい暗黙のコードを導入することになります。関数を読んでも、 終了時にデストラクタが実行されることについて、明確な兆候は得られません。これはデバッグを厄介なものにする可能性があります。
ファイナライズのためだけにオブジェクトと Drop
の実装が必要となり、ボイラーテンプレートが多くなります。
議論
ファイナライザとして使用されるオブジェクトの正確な格納方法については、繊細な点があります。このオブジェクトは関数が終了するまで存続しなければならず、また終了時に破棄されなければなりません。このオブジェクトは常に値かユニークに所有されるポインタでなければなりません (例 Box<Foo>
など) 。もし共有ポインタ(Rc
など)を使用した場合、ファイナライザが関数のライフタイムを超えて存続してしまう可能性があります。同様の理由でファイナライザの所有権を移動したり、ファイナライザを戻り値としてはいけません。
ファイナライザは変数に代入しなければなりません。そうでないとスコープを外れる前に即座に破棄されます。変数名は _
で始まらなければなりません。そうでないとコンパイラは変数が利用されていない旨の警告を出します。しかし変数をサフィックスなしの _
としてはいけません。そうすると、即座に破棄されてしまいます。
Rustでは、デストラクタはオブジェクトがスコープ外に出たときに実行されます。これが起こるのは、ブロックの終了に到達したとき、早期リターンのとき、パニックが発生したときです。パニックが起きると、Rustはスタックフレームの各オブジェクトのデストラクタを実行しつつスタックを巻き戻します。そのため、呼び出し中の関数内にてパニックが発生した場合でも、デストラクタが呼び出されます。
デストラクタが巻き戻し中にパニックを起こした場合、取るべき良いアクションはないため、Rustはそれ以上デストラクタを実行せずにスレッドを即座に終了させます。これは、デストラクタの実行が絶対に保証されているわけではないということです。また、デストラクタがパニックに陥らないように細心の注意を払う必要があるということでもあります。リソースを想定されない状態にする可能性があるからです。
See also
mem::{take(_), replace(_)}
による値を所有したままの列挙値の変換
説明
A { name: String, x: u8 }
と B { name: String }
という(少なくとも) 2つのバリアントを持つ &mut MyEnum
があるとします。MyEnum::A
をその x
が 0 の場合に B
に変更したいとします。このとき MyEnum::B
に手を入れたくないとします。
name
をクローンすることなく、これを実行できます。
例
#![allow(unused)] fn main() { use std::mem; enum MyEnum { A { name: String, x: u8 }, B { name: String }, } fn a_to_b(e: &mut MyEnum) { if let MyEnum::A { name, x: 0 } = e { // これは `name` を取り出し、その代わりに空文字列を置きます。 // (空文字列はメモリが割り当てが発生しないことに注意) // そして、新しい列挙型のバリアントを作成します( `*e` に代入します)。 *e = MyEnum::B { name: mem::take(name), } } } }
より多くのバリアントでも同様です:
#![allow(unused)] fn main() { use std::mem; enum MultiVariateEnum { A { name: String }, B { name: String }, C, D, } fn swizzle(e: &mut MultiVariateEnum) { use MultiVariateEnum::*; *e = match e { // 所有権ルールでは `name` を値として取り出すことはできず、 // 値を置き換えない限り、可変参照から値を取り出すことはできない: A { name } => B { name: mem::take(name), }, B { name } => A { name: mem::take(name), }, C => D, D => C, } } }
動機形成
列挙型を扱うとき、ある列挙型の値を別のバリアントに置き換えたいことがあります。通常これは、借用チェッカーを満足させるために、二段階に分けて行われます。第一段階では、既存の値を参照し、その一部を見て次の処理を決めます。第二段階では、(上の例のように) 条件付きで値を変更することができます。
借用チェッカーは列挙型の name
を取り出すことを許可しません (そこに 何か がなければならないため)。もちろん、 name
を .clone()
して、そのクローンを MyEnum::B
に入れることはできますが、それは 借用チェッカーを満足させるためのClone アンチパターンの実例になってしまいます。いずれにせよ、 可変借用のみで ‘e‘ を変更することで余分なアロケーションを避けることができます。
mem::take
は、値をそのデフォルト値に置き換えるとともに、元の値を返します。これにより値を入れ替える形で取り出せます。String
の場合、デフォルト値は空の String
であり、アロケートの必要がありません。結果として、元の name
を 所有した値として 取得できます。そしてこれを別の列挙型にラップすることができます。
NOTE: mem::replace
は非常に似ていますが、値を何に置き換えるかを指定できます。例示の mem::take
は mem::replace(name, String::new())
に相当します。
ただし Option
を使用していてその値を None
に置き換えたい状況では、 Option
の take()
メソッドを使用することがより短くイディオム的な代替手段であること注意してください。
長所
見て!アロケーション無し!インディ・ジョーンズのような気分も味わえるかも。
短所
少しくどい表現になります。何度も間違えると借用チェッカーが憎くなるかもしれません。コンパイラはダブルストアの最適化に失敗するかもしれず、そうすると unsafe な言語で行うのと比べてパフォーマンスが低下する結果となります。
さらに、扱う型は Default
trait を実装している必要があります。しかしながら、もし実装していない場合は、代わりに mem::replace
を使用することができます。
議論
このパターンが注目されるのはRustだけです。GCのある言語では、デフォルトで値への参照を取ります(そしてGCは参照を追跡します)。Cのような低レベル言語では、単にポインタをエイリアスして後から修正します。
しかしRustでは、これを行うにはもう少し工夫が必要です。所有された値には所有者が1人しかいないので、それを取り出すには、何かを戻す必要があります。インディ・ジョーンズのように、アーティファクトを砂袋に置き換えるのです。
See also
これは 借用チェッカーを満足させるためのClone アンチパターンを特定のケースで取り除くものです。
オンスタックの動的ディスパッチ
説明
複数の値を動的にディスパッチすることは可能ですが、そうするためには、異なる型のオブジェクトに束縛するため複数の変数を宣言する必要があります。ライフタイムを必要に応じて延長するために、遅延条件付き初期化を使うことができます:
例
use std::io; use std::fs; fn main() -> Result<(), Box<dyn std::error::Error>> { let arg = "-"; // これらは `readable` よりも長く生きなければならないため、最初に宣言する: let (mut stdin_read, mut file_read); // 動的ディスパッチを得るためには、型を記述する必要がある。 let readable: &mut dyn io::Read = if arg == "-" { stdin_read = io::stdin(); &mut stdin_read } else { file_read = fs::File::open(arg)?; &mut file_read }; // ここで `readable` から読み込む。 Ok(()) }
動機形成
Rustはデフォルトでコードを単相化します。これは、使用する型毎にコードのコピーが生成され、別個に最適化されることを意味します。これはホットパスにおける非常に高速なコードを実現しますが、パフォーマンスが重要でない場所においてコードが肥大化することにもなります。その結果コンパイル時間やキャッシュの使用量が犠牲になります。
幸いなことに、Rustでは動的ディスパッチが使えますが、明示的に要求する必要があります。
長所
ヒープ上に何もアロケートする必要がありません。後で使わないものを初期化する必要もなく、 File
と Stdin
の両方で動作するように、続くコード全体を単相化する必要もありません。
短所
このコードは、 Box
ベースのバージョンよりも多くの変動部があります:
// 動的なディスパッチには、型を指定する必要がある。
let readable: Box<dyn io::Read> = if arg == "-" {
Box::new(io::stdin())
} else {
Box::new(fs::File::open(arg)?)
};
// ここで `readable` から読み込む。
議論
Rustの初心者は通常、Rustはすべての変数を 使用前に 初期化する必要があることを学びます。そのため、 未使用 変数が初期化されていなくともよいという事実を見落としがちです。Rust は、これが問題なく動作するよう相当な努力を払って保証しており、スコープの最後には初期化済みの値のみがdropされます。
この例は、Rust が私たちに課しているすべての制約を満たしています:
- すべての変数は、使用する(この場合は借用される)前に初期化されます
- 各変数は単一の型の値のみを保持します。この例では、
stdin_read
はStdin
型、file_read
はFile
型、readable
は&mut dyn Read
型です - 借用された各値は、それから借用されたすべての参照よりも長生きします
See also
- デストラクタでのファイナライズ とRAIIガードは、ライフタイムの厳密な制御から恩恵を得ます。
- 条件により中身が決まる参照 (可変のもの含む) の
Option<&T>
などに対しては、次のようにします。Option<T>
を直接初期化し、その [.as_ref()
] メソッドを使用することでOptionにくるまれた参照を取得できます。
FFIのイディオム
FFIコードを書くことは、それ自体が1つの課程です。しかしながら、ポインタの役割を担い、unsafe
Rustの経験の浅いユーザーが陥る罠を回避する、いくつかのイディオムがあります。
このセクションでは、FFIを行う際に役立つイディオムを紹介します。
FFI のエラー処理
説明
C言語のような言語では、エラーはリターンコードで表されます。しかしRustの型システムは、よりリッチなエラー情報を完全な型を通して捕捉、伝播することが可能です。
このベストプラクティスでは、さまざまな種類のエラーコードを示し、どのようにそれらを扱いやすい方法で公開するかを示します:
- フラットな列挙型は整数に変換してコードとして返します。
- 構造化された列挙型は、詳細についての文字列のエラーメッセージとともに、整数コードに変換されるべきです。
- カスタムエラー型は、C言語で表現された、“透過的 “なものになるべきです。
コードの例
フラットな列挙型
enum DatabaseError {
IsReadOnly = 1, // ユーザーが書き込み操作を試みた
IOError = 2, // ユーザーは C の errno() が何であったかを確認すべきである
FileCorrupted = 3, // ユーザーは修復ツールを実行して回復する必要がある
}
impl From<DatabaseError> for libc::c_int {
fn from(e: DatabaseError) -> libc::c_int {
(e as i8).into()
}
}
構造化された列挙型
pub mod errors {
enum DatabaseError {
IsReadOnly,
IOError(std::io::Error),
FileCorrupted(String), // 問題を説明するメッセージ
}
impl From<DatabaseError> for libc::c_int {
fn from(e: DatabaseError) -> libc::c_int {
match e {
DatabaseError::IsReadOnly => 1,
DatabaseError::IOError(_) => 2,
DatabaseError::FileCorrupted(_) => 3,
}
}
}
}
pub mod c_api {
use super::errors::DatabaseError;
#[no_mangle]
pub extern "C" fn db_error_description(e: *const DatabaseError) -> *mut libc::c_char {
let error: &DatabaseError = unsafe {
// SAFETY: ポインタの寿命が現在のスタックフレームより大きい
&*e
};
let error_str: String = match error {
DatabaseError::IsReadOnly => {
format!("cannot write to read-only database");
}
DatabaseError::IOError(e) => {
format!("I/O Error: {e}");
}
DatabaseError::FileCorrupted(s) => {
format!("File corrupted, run repair: {}", &s);
}
};
let c_error = unsafe {
// SAFETY: error_strを、アローケートしたバッファに
// NUL文字終端を付けてコピーする
let mut malloc: *mut u8 = libc::malloc(error_str.len() + 1) as *mut _;
if malloc.is_null() {
return std::ptr::null_mut();
}
let src = error_str.as_bytes().as_ptr();
std::ptr::copy_nonoverlapping(src, malloc, error_str.len());
std::ptr::write(malloc.add(error_str.len()), 0);
malloc as *mut libc::c_char
};
c_error
}
}
カスタムエラー型
struct ParseError {
expected: char,
line: u32,
ch: u16,
}
impl ParseError {
/* ... */
}
/* C構造体として公開される2つめのバージョンを作成する */
#[repr(C)]
pub struct parse_error {
pub expected: libc::c_char,
pub line: u32,
pub ch: u16,
}
impl From<ParseError> for parse_error {
fn from(e: ParseError) -> parse_error {
let ParseError { expected, line, ch } = e;
parse_error { expected, line, ch }
}
}
長所
これにより、RustコードのAPIをまったく損なうことなく、他言語でもエラー情報にアクセスできるようになります。
短所
タイピングが多くなります。またC言語に簡単に変換できない型もあります。
文字列の受け入れ
説明
FFIから文字列をポインタを通して受け取る場合、次の2つの原則に従う必要があります:
- 外部からの文字列を直接コピーするのではなく、「借用」しておいてください。
- Cスタイルの文字列から Rust ネイティブの文字列への変換に伴う複雑さと
unsafe
コードの量を最小限にしてください。
動機形成
C言語で使用される文字列は、Rustで使用される文字列とは振る舞いが異なります。即ち:
- C言語の文字列はヌル終端であるのに対し、Rustの文字列は長さを保存します
- C言語の文字列は0以外の任意のバイトを含むことができますが、Rust の文字列は UTF-8 でなければなりません
- C の文字列は
unsafe
ポインタ操作を使ってアクセスおよび操作されます。一方、Rust の文字列とのやり取りは安全なメソッドを使用します
Rustの標準ライブラリには、Rustの String
と &str
に相当するC言語の文字列版が用意されており、 CString
と &CStr
と呼びます。これらにより C言語の文字列と Rust の文字列の変換にかかわる複雑さの大部分と unsafe
なコードを避けることができます。
また、 &CStr
型は、借用データを扱うことを可能とします。これは Rust とC言語の間での文字列の受け渡しがゼロコストで行えるということです。
コードの例
pub mod unsafe_module {
// モジュールの他のコンテンツ
/// 指定されたレベルでメッセージをログに記録する。
///
/// # Safety
///
/// 呼び出し側は `msg` について以下を保証する:
///
/// - null ポインタではない
/// - 有効で初期化されたデータを指す
/// - null バイトで終わるメモリを指す
/// - この関数呼び出しの間、変更されない
#[no_mangle]
pub unsafe extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
let level: crate::LogLevel = match level { /* ... */ };
// SAFETY: 呼び出し側は、これが問題ないことを既に保証している
// (この doc-comment の `# Safety` セクションを参照)。
let msg_str: &str = match std::ffi::CStr::from_ptr(msg).to_str() {
Ok(s) => s,
Err(e) => {
crate::log_error("FFI string conversion failed");
return;
}
};
crate::log(msg_str, level);
}
}
長所
この例は、以下のことを担保するように記述されています:
unsafe
ブロックは可能な限り小さくします。- 「追跡されない」ライフタイムのポインタを「追跡される」共有参照にします
別の方法として、文字列を実際にコピーする方法を考えてみましょう:
pub mod unsafe_module {
// モジュールの他のコンテンツ
pub extern "C" fn mylib_log(msg: *const libc::c_char, level: libc::c_int) {
// このコードは使わないでください。
// このコードは醜く、冗長で、微妙なバグを含んでいます。
let level: crate::LogLevel = match level { /* ... */ };
let msg_len = unsafe { /* SAFETY: strlenはその通りのものだ、と思った? */
libc::strlen(msg)
};
let mut msg_data = Vec::with_capacity(msg_len + 1);
let msg_cstr: std::ffi::CString = unsafe {
// SAFETY: スタックフレーム全体が生きていると
// 期待される外部ポインタから、所有メモリへのコピー
std::ptr::copy_nonoverlapping(msg, msg_data.as_mut(), msg_len);
msg_data.set_len(msg_len + 1);
std::ffi::CString::from_vec_with_nul(msg_data).unwrap()
}
let msg_str: String = unsafe {
match msg_cstr.into_string() {
Ok(s) => s,
Err(e) => {
crate::log_error("FFI string conversion failed");
return;
}
}
};
crate::log(&msg_str, level);
}
}
このコードは2つの点で元のものより劣っています:
unsafe
コードがより多くあり、さらに重要なこととして、維持する必要のある不変性がより多くあります。- 追加の計算が必要となったことにより、このバージョンには Rust の
undefined behavior
を引き起こすバグがあります。
このバグはポインタ演算の単純なミスです: 文字列は msg_len
バイトすべてをコピーされましたが、末尾の NUL
終端がコピーされませんでした。
ベクタのサイズは、 ゼロパディングされた文字列 の長さに 設定 されています – 終端に 0 を追加できるよう、 リサイズ されるのではなく。その結果、ベクタの最後のバイトは未初期化のメモリになります。ブロックの最後にて CString
を作成する際、このベクタの読み出しが undefined behaviour
を引き起こします!
このような問題の多くがそうであるように、これは突き止めることが難しい問題となるでしょう。ときには文字列が UTF-8
でなかったためにパニックが発生したり、またときには文字列の末尾に変な文字が置かれたり、または完全にクラッシュしたりすることでしょう。
短所
なし?
文字列の受け渡し
説明
FFI関数に文字列を渡す場合、次の4つの原則に従う必要があります:
- 所有している文字列のライフタイムを可能な限り長くします。
- 変換の間の
unsafe
コードを最小限に抑えます。 - Cのコードが文字列データを変更し得る場合は、
CString
の代わりにVec
を使用します。 - 外部関数APIがそれを要求しない限り、文字列の所有権は呼び出し側に移してはいけません。
動機形成
Rust は CString
型と CStr
型により C スタイルの文字列をビルトインでサポートしています。しかしながら、Rust 関数から外部関数を呼び出す際に受け渡す文字列に関しては、さまざまなアプローチがありえます。
ベストプラクティスは単純です: unsafe
コードを最小限にするように CString
を使うことです。ただし、次いでの注意点があり、それは オブジェクトは十分に長く生存する必要がある ことです。これはライフタイムを最大化すべきという意味です。加えてドキュメントでは、変更後の CString
を 「ラウンドトリップ」 することは未定義動作であると説明されており、このようなケースでは追加の対応が必要です。
コードの例
pub mod unsafe_module {
// モジュールの他のコンテンツ
extern "C" {
fn seterr(message: *const libc::c_char);
fn geterr(buffer: *mut libc::c_char, size: libc::c_int) -> libc::c_int;
}
fn report_error_to_ffi<S: Into<String>>(err: S) -> Result<(), std::ffi::NulError> {
let c_err = std::ffi::CString::new(err.into())?;
unsafe {
// SAFETY: ポインタがconstであるとドキュメントに記述されているFFIの呼び出し、
// すなわち変更されることはない
seterr(c_err.as_ptr());
}
Ok(())
// c_err のライフタイムはここまで続く
}
fn get_error_from_ffi() -> Result<String, std::ffi::IntoStringError> {
let mut buffer = vec![0u8; 1024];
unsafe {
// SAFETY: calling an FFI whose documentation implies
// that the input need only live as long as the call
let written: usize = geterr(buffer.as_mut_ptr(), 1023).into();
buffer.truncate(written + 1);
}
std::ffi::CString::new(buffer).unwrap().into_string()
}
}
長所
この例は、以下のことを担保するように記述されています:
unsafe
ブロックは可能な限り小さくします。CString
は十分に長生きします。- 型キャストによるエラーは、可能な限り常に伝播されます。
よくある間違い(ドキュメントにも載っているほど)は、最初のブロックで変数を使わないことです:
pub mod unsafe_module {
// モジュールの他のコンテンツ
fn report_error<S: Into<String>>(err: S) -> Result<(), std::ffi::NulError> {
unsafe {
// SAFETY: おっと、これはダングリングポインタを含んでいる!
seterr(std::ffi::CString::new(err.into())?.as_ptr());
}
Ok(())
}
}
このコードではダングリングポインタが発生します。参照の作成とは異なり、ポインタの作成によって CString
の寿命が延長されることはないからです。
もうひとつよく挙げられる問題は、1kのゼロ埋めベクトルの初期化が「遅い」ということです。しかし、Rustの最近のバージョンでは、この特定のマクロはなんと zmalloc
の呼び出しに最適化されています。これは、オペレーティングシステムのゼロ埋めしたメモリを返す機能 (これはかなり高速です) と同じ速さであることを意味します。
短所
なし?
Option
に対する反復処理
説明
Option
は、0個または1個の要素を含むコンテナとみなすことができます。特にこれは IntoIterator
トレイトを実装しています。よってそのような型を必要とするジェネリックなコードにて利用できます。
例
Option
は IntoIterator
を実装しているので、.extend()
の引数として使用できます:
#![allow(unused)] fn main() { let turing = Some("Turing"); let mut logicians = vec!["Curry", "Kleene", "Markov"]; logicians.extend(turing); // 以下と等しい if let Some(turing_inner) = turing { logicians.push(turing_inner); } }
既存のイテレータの末尾に Option
を追加する必要がある場合、 .chain()
に渡せます:
#![allow(unused)] fn main() { let turing = Some("Turing"); let logicians = vec!["Curry", "Kleene", "Markov"]; for logician in logicians.iter().chain(turing.iter()) { println!("{logician} is a logician"); } }
もし Option
が常に Some
である場合は、その要素に対して std::iter::once
を使用する方がより慣習的であることに注意してください。
また、Option
は IntoIterator
を実装しているので、for
ループを使って繰り返し処理を行うことができます。これは if let Some(...)
とマッチさせるのと同じです。ほとんどの場合、後者の方がよいでしょう。
See also
-
[std::iter::once
](https://doc.rust-lang.org/std/iter/fn.once.html)は、厳密に1つの要素を返すイテレータです。Some(foo).into_iter()
に代わる、よりわかりやすいイテレータです。 -
Iterator::filter_map
はIterator::map
のバージョンで、Option
を返すマッピング関数に特化したものです。 -
ref_slice
はOption
を 0 要素または 1 要素のスライスに変換する関数を提供します。
変数をクロージャに渡す
説明
デフォルトでは、クロージャは借用する形で環境をキャプチャします。あるいは環境全体をムーブする move
クロージャを使うこともできます。しかしながら、クロージャへ一部の変数のみを対象に、ムーブさせる/データのコピーを与える/参照渡しをする/その他の変換を行う、といったことをしたいことがよくあります。
Use variable rebinding in a separate scope for that.
例
次の例のようにしましょう
#![allow(unused)] fn main() { use std::rc::Rc; let num1 = Rc::new(1); let num2 = Rc::new(2); let num3 = Rc::new(3); let closure = { // num1` はムーブされる let num2 = num2.clone(); // num2` がクローンされる let num3 = num3.as_ref(); // num3` は借用される move || { *num1 + *num2 + *num3; } }; }
上の例は下の例と同等の意味となります
#![allow(unused)] fn main() { use std::rc::Rc; let num1 = Rc::new(1); let num2 = Rc::new(2); let num3 = Rc::new(3); let num2_cloned = num2.clone(); let num3_borrowed = num3.as_ref(); let closure = move || { *num1 + *num2_cloned + *num3_borrowed; }; }
長所
コピーされたデータは、クロージャの定義と一緒にグループ化されるため、その目的がより明確になります。また、もしそれらがクロージャによって消費されない場合でも、即座に削除されることになります。
クロージャは、データがコピーされようがムーブされようが、周辺のコードと同じ変数名を使用します。
短所
クロージャの本体のインデントが増えます。
拡張性のための #[non_exhaustive]
と private なフィールド
説明
ライブラリの作者が後方互換性を破壊することなく、publicな構造体へpublicなフィールドを追加したり、enumへ新しいバリアントを追加したりしたい、といったいくつかのシナリオがあります。
Rust はこの問題に対して2つの解決策を提供します:
-
struct
やenum
、enum
バリアントに対して#[non_exhaustive]
を使用する。#[non_exhaustive]
が使用できるすべての場所に関する詳細なドキュメントは the docs. を参照してください。 -
構造体が直接インスタンス化されたり、マッチングされたりしないようにするために、構造体に private なフィールドを追加することができます(Alternative 参照)
例
#![allow(unused)] fn main() { mod a { // public な構造体 #[non_exhaustive] pub struct S { pub foo: i32, } #[non_exhaustive] pub enum AdmitMoreVariants { VariantA, VariantB, #[non_exhaustive] VariantC { a: String, }, } } fn print_matched_variants(s: a::S) { // S は `#[non_exhaustive]` なので、ここで名前を付けることはできず、 // パターンの中で `..` を使わなければならない。 let a::S { foo: _, .. } = s; let some_enum = a::AdmitMoreVariants::VariantA; match some_enum { a::AdmitMoreVariants::VariantA => println!("it's an A"), a::AdmitMoreVariants::VariantB => println!("it's a b"), // このバリアントも非網羅的であるため .. が必須 a::AdmitMoreVariants::VariantC { a, .. } => println!("it's a c"), // 将来的にバリアントが追加される可能性があるため、 // ワイルドカードによるマッチが必須 _ => println!("it's a new variant"), } } }
Alternative: 構造体の private なフィールド
#[non_exhaustive]
が機能するのは、crate の境界を越えるときのみです。クレート内部では、private なフィールドによる手法を使用できます。
構造体にフィールドを追加することはだいたいの場合、後方互換性のある変更です。ただし、構造体のクライアントが、パターンを使用して構造体のインスタンスをデコンストラクトする際に、構造体内のすべてのフィールドに名前を付けている可能性があります。この状況で構造体に新しいフィールドを追加することは、そのパターンを壊すことになります。クライアントは、パターンの中でいくつかのフィールドに名前を付けるとともに ..
を使うことが可能であり、この場合はフィールドの追加に対して後方互換性があります。構造体のフィールドの1つ以上を private なものにすることで、クライアントに後者のパターンを使用することを強制することが可能であり、構造体の将来性を確保できます。
この方法の欠点は、構造体に本来不要なフィールドを追加する必要がある可能性があることです。()
型を使うことでランタイムのオーバーヘッドを回避できます。またフィールド名の頭に _
を付けることで未使用フィールドの警告を回避できます。
#![allow(unused)] fn main() { pub struct S { pub a: i32, // `b` は private です。 `S` に対して `..` を使用せずにパターンマッチすることは不可能であり、 // `S` を直接インスタンス化したり、マッチ対象としたりすること不可能です。 _b: (), } }
議論
struct
に対しては #[non_exhaustive]
は、後方互換性のある形でフィールドを追加可能にします。また、すべてのフィールドがpublicであっても、クライアントは構造体のコンストラクタを使用できなくなります。これは便利ではあります。しかしながら、フィールドの追加がコンパイルエラーを起こすようにすることで、エラーを見たクライアントがフィールドが追加されたことに気づけるようにする形が (フィールドの追加をクライアントに隠す形よりも) 期待 されることもあります。
#[non_exhaustive]
は列挙型のバリアントにも適用できます。#[non_exhaustive]
なバリアントは、#[non_exhaustive]
な構造体と同様に振舞います。
この方法は計画的かつ注意深く使用してください: フィールドやバリアントを追加するときは、 メジャーバージョンをインクリメントした方が良いことが多いのです。#[non_exhaustive]
は、あなたのライブラリが、ライブラリに同期させることができない外部のリソースをモデリングする場合に適切かもしれませんが、汎用目的のツールではありません。
短所
#[non_exhaustive]
はコードを人間工学的に使いにくくします。特に未知の列挙型のバリアントを扱わなければならない場合は顕著です。この種の開発を、メジャーバージョンをインクリメント せずに 行う必要がある場合のみ使うべきです。
enum
に #[non_exhaustive]
を適用することは、クライアントにワイルドカードの使用を強制します。この場合に取るべき賢明なアクションがなければ、厄介なコードや、極めて稀な状況でのみ実行されるコードパスを生み出しかねません。クライアントがこのシナリオで panic!()
を選択するようなら、コンパイル時エラーとして公開された方がよかったかもしれません。実際、#[non_exhaustive]
はクライアントに 「その他の何か」 に対するケースを処理するように強制します。そして、このシナリオで取るべき賢明なアクションはまずありません。
See also
docコメント内の簡易な初期化
説明
docコメントを書く際に、構造体を初期化するのに多大な労力がかかる場合は、構造体を引数に取るヘルパー関数でコード例をラップした方が手っ取り早いかもしれません。
動機形成
多数の、または複雑なパラメータや複数のメソッドが、構造体に存在するとしましょう。これらのメソッドにはそれぞれコード例があるはずです。
例:
struct Connection {
name: String,
stream: TcpStream,
}
impl Connection {
/// 接続を介してリクエストを送信
///
/// # Example
/// ```no_run
/// # // コード例を機能させるにはボイラーテンプレートが必要になります。
/// # let stream = TcpStream::connect("127.0.0.1:34254");
/// # let connection = Connection { name: "foo".to_owned(), stream };
/// # let request = Request::new("RequestId", RequestType::Get, "payload");
/// let response = connection.send_request(request);
/// assert!(response.is_ok());
/// ```
fn send_request(&self, request: Request) -> Result<Status, SendErr> {
// ...
}
/// なんと、ここでも同じボイラーテンプレートが必要です!
fn check_status(&self) -> Status {
// ...
}
}
例
このような Connection
や Request
を生成するボイラーテンプレートすべてをタイピングする代わりに、それらを引数として受け取るヘルパー関数をラッパーとして作成する方が簡単です:
struct Connection {
name: String,
stream: TcpStream,
}
impl Connection {
/// 接続を介してリクエストを送信
///
/// # Example
/// ```
/// # fn call_send(connection: Connection, request: Request) {
/// let response = connection.send_request(request);
/// assert!(response.is_ok());
/// # }
/// ```
fn send_request(&self, request: Request) {
// ...
}
}
注意 上記の例における、 assert!(response.is_ok());
という行は、呼び出されない関数内にあるため、実際にはテスト中に実行されません。
長所
この方がはるかに簡潔で、コード例中のコードの繰り返しを避けることができます。
短所
コード例が関数定義の中にあるので、コードはテストされません。しかし cargo test
を実行することで、コンパイルできたかどうかはチェックされます。ですから、このパターンは no_run
が必要なときに便利です。このパターンを使う場合、 (実行されないため) no_run
を追加する必要はありません。
議論
アサーションが必要ない場合は、このパターンがうまく機能します。
もしアサーションが必要であるなら、別の方法として、ヘルパーインスタンスを作成するpublicメソッドを #[doc(hidden)]
でアノテーションして作成する方法があります。これにより、このメソッドはcrateのパブリックAPIの一部として、 rustdoc の内部で呼び出すことが可能になります。
一時的なミュータビリティ
説明
あるデータを準備・処理する必要があり、いったんそれが済めば、データは参照されるだけであり、変更されることはない、という状況がよくあります。このような状況では、可変変数を不変変数として再定義することで、その意図を明示することができます。
これは、ネストされたブロック内でデータを処理することか、変数を再束縛することで実現できます。
例
ベクタが使用される前にソートする必要がある、という状況を考えましょう。
ネストされたブロックを使う場合:
let data = {
let mut data = get_vec();
data.sort();
data
};
// ここで `data` は不変である。
変数の再束縛を使う場合:
let mut data = get_vec();
data.sort();
let data = data;
// ここで `data` は不変である。
長所
ある時点以降にデータを誤って変更してしまうことを、コンパイラが防ぎます。
短所
ネストされたブロックは、ブロックの本体に追加のインデントが必要になります。ブロックからデータを返したり、変数を再定義するに、追加の1行が必要です。
エラー時に消費した引数を返す
説明
失敗する可能性のある関数が引数を消費(move)した場合、その引数をエラーの中に返却しましょう。
例
pub fn send(value: String) -> Result<(), SendError> { println!("using {value} in a meaningful way"); // 非決定的な失敗し得るアクションをシミュレート。 use std::time::SystemTime; let period = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); if period.subsec_nanos() % 2 == 1 { Ok(()) } else { Err(SendError(value)) } } pub struct SendError(String); fn main() { let mut value = "imagine this is very long string".to_string(); let success = 's: { // value を2回送信してみる。 for _ in 0..2 { value = match send(value) { Ok(()) => break 's true, Err(SendError(value)) => value, } } false }; println!("success: {success}"); }
動機形成
エラーが発生した際、なにか別の手段を試したり、非決定的な関数であるならリトライしたくなるものです。しかし、もし引数が常に消費されてしまうのであれば、呼び出しのたびに引数をクローンしなければならなくなります。これではとても効率的とは言えません。
標準ライブラリでは、このアプローチを String::from_utf8
メソッドなどで使用しています。有効な UTF-8 を含まないベクタが与えられた場合、 FromUtf8Error
を返します。 FromUtf8Error::into_bytes
メソッドを使用することで、元のベクタを取得できます。
長所
可能な限り引数をmoveすることで、パフォーマンスを向上させます。
短所
エラーの型がやや複雑になります。
デザインパターン
デザインパターン とは「general reusable solutions to a commonly occurring problem within a given context in software design(ソフトウェア設計において、与えられた文脈の中で一般的に発生する問題に対する一般的で再利用可能な解決策)」です。デザインパターンはプログラミング言語の文化を表現する素晴らしい方法です。デザインパターンはかなり言語固有のもので - ある言語ではパターンであっても、別の言語ではその言語の特徴によって不要であったり、欠けている機能によって表現できなかったりします。
デザインパターンは使い過ぎると、プログラムに不必要な複雑さを加えることがあります。しかし、デザインパターンは、プログラミング言語に関する中級および上級レベルの知識を共有するための素晴らしい方法です。
Rustのデザインパターン
Rust には多くのユニークな特徴があります。これらの機能は、各種問題の大半を取り除く、大きなメリットをもたらします。その中には、Rust 独自 のパターンもあります。
YAGNI
YAGNIとは「You Aren’t Going to Need It」の頭文字をとったものです。これは、コードを書くときに適用すべき重要なソフトウェア設計の原則です。
これまで私が書いた中で最高のコードは、書かなかったコードです。
YAGNIをデザインパターンに適用すると、Rustの機能によって多くのパターンを捨てることができることがわかります。例えば、Rustでは strategyパターン は不要です。なぜなら、単に trait を使えば良いためです。
振る舞いに関するパターン
Wikipedia より:
Design patterns that identify common communication patterns among objects. By doing so, these patterns increase flexibility in carrying out communication. (オブジェクト間で共通するコミュニケーションのパターンを特定するデザインパターン。これらのパターンの実践により、コミュニケーションを実行する際の柔軟性が高まります。)
Command
説明
Commandパターンの基本的な考え方は、アクションを独自のオブジェクトに分離し、それらをパラメータとして渡すことです。
動機形成
オブジェクトとしてカプセル化された一連のアクションやトランザクションがあるとします。これらのアクションやコマンドを、ある順序で、後の時間に実行したり呼び出したりしたいとします。これらのコマンドは、何らかのイベントの結果としてトリガーされることもあるでしょう。例えば、ユーザーがボタンを押した時や、データパケットが到着した時などのように。また他に、これらのコマンドはアンドゥ可能であるかもしれません。これはエディターの操作などに役立つでしょう。また実行したコマンドのログを保存しておき、後でシステムがクラッシュしたときにその変更を再適用できるようにしておきたいこともあるでしょう。
例
create table
と add field
の2つのデータベース操作を定義します。これらの操作はそれぞれこれらの操作はそれぞれ、そのコマンドをアンドゥする方法(例えば drop table
と remove field
)が分かっているコマンドです。ユーザがデータベースの移行操作を実行すると、各コマンドが定義された順序で実行され、またロールバック操作を実行すると、コマンド一式が逆の順序で実行されます。
アプローチ:トレイトオブジェクトの使用
execute
と rollback
の2つの操作を持つコマンドをカプセル化する共通のトレイトを定義します。すべてのコマンドの struct
はこのトレイトを実装しなければなりません。
pub trait Migration { fn execute(&self) -> &str; fn rollback(&self) -> &str; } pub struct CreateTable; impl Migration for CreateTable { fn execute(&self) -> &str { "create table" } fn rollback(&self) -> &str { "drop table" } } pub struct AddField; impl Migration for AddField { fn execute(&self) -> &str { "add field" } fn rollback(&self) -> &str { "remove field" } } struct Schema { commands: Vec<Box<dyn Migration>>, } impl Schema { fn new() -> Self { Self { commands: vec![] } } fn add_migration(&mut self, cmd: Box<dyn Migration>) { self.commands.push(cmd); } fn execute(&self) -> Vec<&str> { self.commands.iter().map(|cmd| cmd.execute()).collect() } fn rollback(&self) -> Vec<&str> { self.commands .iter() .rev() // イテレータの方向を逆にする .map(|cmd| cmd.rollback()) .collect() } } fn main() { let mut schema = Schema::new(); let cmd = Box::new(CreateTable); schema.add_migration(cmd); let cmd = Box::new(AddField); schema.add_migration(cmd); assert_eq!(vec!["create table", "add field"], schema.execute()); assert_eq!(vec!["remove field", "drop table"], schema.rollback()); }
アプローチ:関数ポインタの使用
他のアプローチとして、個々のコマンドを別個の関数として作成し、関数ポインタを格納、後でこれらの関数を別の時間に呼び出す方法があります。関数ポインタは Fn
、 FnMut
、 FnOnce
の3つのトレイトすべてを実装しているので、関数ポインタの代わりにクロージャを渡して格納することもできます。
type FnPtr = fn() -> String; struct Command { execute: FnPtr, rollback: FnPtr, } struct Schema { commands: Vec<Command>, } impl Schema { fn new() -> Self { Self { commands: vec![] } } fn add_migration(&mut self, execute: FnPtr, rollback: FnPtr) { self.commands.push(Command { execute, rollback }); } fn execute(&self) -> Vec<String> { self.commands.iter().map(|cmd| (cmd.execute)()).collect() } fn rollback(&self) -> Vec<String> { self.commands .iter() .rev() .map(|cmd| (cmd.rollback)()) .collect() } } fn add_field() -> String { "add field".to_string() } fn remove_field() -> String { "remove field".to_string() } fn main() { let mut schema = Schema::new(); schema.add_migration(|| "create table".to_string(), || "drop table".to_string()); schema.add_migration(add_field, remove_field); assert_eq!(vec!["create table", "add field"], schema.execute()); assert_eq!(vec!["remove field", "drop table"], schema.rollback()); }
アプローチ: Fn
トレイトオブジェクトの使用
最後に、共通のコマンドのトレイトを定義する代わりに、個々に Fn
トレイトを実装した各コマンドをベクタに格納することができます。
type Migration<'a> = Box<dyn Fn() -> &'a str>; struct Schema<'a> { executes: Vec<Migration<'a>>, rollbacks: Vec<Migration<'a>>, } impl<'a> Schema<'a> { fn new() -> Self { Self { executes: vec![], rollbacks: vec![], } } fn add_migration<E, R>(&mut self, execute: E, rollback: R) where E: Fn() -> &'a str + 'static, R: Fn() -> &'a str + 'static, { self.executes.push(Box::new(execute)); self.rollbacks.push(Box::new(rollback)); } fn execute(&self) -> Vec<&str> { self.executes.iter().map(|cmd| cmd()).collect() } fn rollback(&self) -> Vec<&str> { self.rollbacks.iter().rev().map(|cmd| cmd()).collect() } } fn add_field() -> &'static str { "add field" } fn remove_field() -> &'static str { "remove field" } fn main() { let mut schema = Schema::new(); schema.add_migration(|| "create table", || "drop table"); schema.add_migration(add_field, remove_field); assert_eq!(vec!["create table", "add field"], schema.execute()); assert_eq!(vec!["remove field", "drop table"], schema.rollback()); }
議論
コマンドが小さく、関数として定義されたり、クロージャとして渡されたりするのであれば、関数ポインタを使うのが望ましいでしょう。動的ディスパッチを濫用せずに済みます。しかし、コマンドが、別のモジュールに定義されている関数や変数の多い構造体全体である場合は、 トレイトオブジェクトを使用するのが望ましいでしょう。その応用例が actix
です。これは、ルート用のハンドラ関数を登録するときにトレイトオブジェクトを使用しています。Fn
トレイトオブジェクトを使用する場合、関数ポインタを使用するの場合と同じようにコマンドを作成して使用することができます。
パフォーマンスとコードのシンプルさと秩序の間には常にトレードオフがあります。静的ディスパッチはより高速なパフォーマンスをもたらしますが、動的ディスパッチはアプリケーションを構成する際に柔軟性をもたらします。
See also
Interpreter
説明
ある問題が非常に頻繁に発生し、その解決に長く反復的な手順を必要とする場合、その問題のインスタンスは単純な言語で表現可能であり、インタープリタ・オブジェクトがこの言語による記述を解釈するという形で解決可能であるかもしれません。
基本的に、あらゆる種類の問題に対して、以下を定義します:
- ドメイン固有言語、
- この言語のための文法、
- 問題インスタンスを解決するインタプリタ。
動機形成
私たちの目標は、簡単な数式を後置記法 (逆ポーランド記法 に変換することです。簡略にするため、式は10桁の数字 0
, …, 9
と2つの演算 +
, -
で構成されるとします。例えば 2 + 4
という式は 2 4 +
に変換されます。
私たちの問題に対する文脈自由文法
私たちのタスクは中置記法を後置記法に変換することです。0
, …, 9
, +
, -
に対応する中置記法の式の集合に対して文脈自由文法を以下に定義します:
- 終端記号:
0
,...
,9
,+
,-
- 非終端記号:exp
、
term` - 開始記号は
exp
- そして、以下が生成ルール
exp -> exp + term
exp -> exp - term
exp -> term
term -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
注: この文法は、これによって私たちが何をしようとしているかによって、さらに変形されるべきです。例えば、左再帰を削除する必要があるかもしれません。詳しくはCompilers: Principles,Techniques, and Tools (別名 Dragon Book)を参照してください。
解決方法
再帰降下パーサを実装するだけです。簡略にするために、構文的に誤った式があればコードはパニックします(例えば、 2-34
や 2+5-
は文法定義に照らすと誤りです)。
pub struct Interpreter<'a> { it: std::str::Chars<'a>, } impl<'a> Interpreter<'a> { pub fn new(infix: &'a str) -> Self { Self { it: infix.chars() } } fn next_char(&mut self) -> Option<char> { self.it.next() } pub fn interpret(&mut self, out: &mut String) { self.term(out); while let Some(op) = self.next_char() { if op == '+' || op == '-' { self.term(out); out.push(op); } else { panic!("Unexpected symbol '{op}'"); } } } fn term(&mut self, out: &mut String) { match self.next_char() { Some(ch) if ch.is_digit(10) => out.push(ch), Some(ch) => panic!("Unexpected symbol '{ch}'"), None => panic!("Unexpected end of string"), } } } pub fn main() { let mut intr = Interpreter::new("2+3"); let mut postfix = String::new(); intr.interpret(&mut postfix); assert_eq!(postfix, "23+"); intr = Interpreter::new("1-2+3-4"); postfix.clear(); intr.interpret(&mut postfix); assert_eq!(postfix, "12-3+4-"); }
議論
Interpreter デザインパターンに対して「形式言語の文法を設計し、その文法に対応したパーサの実装することである」という誤った認識があるかもしれません。実際には、このパターンは問題インスタンスをより具体的に表現し、その問題インスタンスを解決する関数/クラス/構造を実装するというものです。Rust には macro_rules!
があり、これにより特別な構文と、この構文をソースコードへ展開するためのルールを定義することができます。
次の例では、 n
次元ベクタのユークリッド距離 を計算する、単純な macro_rules!
を作成します。norm!(x,1,2)
と記述するのは、 x,1,2
を Vec
に詰め込んで、距離を計算する関数を呼び出すよりも表現が簡単で効率的です。
macro_rules! norm { ($($element:expr),*) => { { let mut n = 0.0; $( n += ($element as f64)*($element as f64); )* n.sqrt() } }; } fn main() { let x = -3f64; let y = 4f64; assert_eq!(3f64, norm!(x)); assert_eq!(5f64, norm!(x, y)); assert_eq!(0f64, norm!(0, 0, 0)); assert_eq!(1f64, norm!(0.5, -0.5, 0.5, -0.5)); }
See also
NewType
ある型に別の型と似たような振る舞いをさせたり、コンパイル時にある振る舞いを強制させたりしたいが、型エイリアスだけでは不十分な場合どのようにすればよいでしょうか?
例えば、セキュリティを考慮(パスワード等)して、String
に対しカスタムした Display
の実装を作成したい場合があります。
このような場合、 Newtype
パターンを使うことで、 型の安全性 と カプセル化 を提供できます。
説明
単一のフィールドを持つタプル構造体を使用して、型の不透明なラッパーを作成します。これは、型のエイリアス (type
によるもの) ではなく、新しい型を作成します。
例
use std::fmt::Display; // String に対し、 Display トレイトをオーバーライドする ニュータイプ Password を作成する struct Password(String); impl Display for Password { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "****************") } } fn main() { let unsecured_password: String = "ThisIsMyPassword".to_string(); let secured_password: Password = Password(unsecured_password.clone()); println!("unsecured_password: {unsecured_password}"); println!("secured_password: {secured_password}"); }
unsecured_password: ThisIsMyPassword
secured_password: ****************
動機形成
ニュータイプの主な動機は抽象化です。これによってインタフェースを正確に制御しながら、型間で実装の詳細を共有することができます。実装型をAPIの一部としてさらけ出すのではなく、 ニュータイプを使うことで、後方互換性を保ったまま実装を変更することができます。
ニュータイプは単位を区別するために使用できます。例えば f64
をラップすることで、Miles
と Kilometers
を区別します。
長所
ラップ対象の型とラップする側の型の間には、(type
を使うのとは対照的に) 型互換性はありません。そのため、 ニュータイプのユーザがこれらの型を「混同」することはありません。
ニュータイプはゼロコスト抽象化です。実行時のオーバーヘッドはありません。
プライバシーシステムは、 ユーザがラップされた型にアクセスできないことを保証します (フィールドがprivateである場合。既定では private)。
短所
newtype の欠点は (特に型のエイリアスと比べて)、特別な言語サポートがないことです。つまり、 多くの ボイラーテンプレートが必要になります。ラップされた型の持つメソッドを公開するには「パススルー」メソッドが必要です。またラッパー型に対し実装したいトレイトについても同様です。
議論
Rustのコードでは、ニュータイプがよく使われます。抽象化や単位の表現が最も一般的な使用法ですが、その他の理由でも使用できます:
- 機能を制限する(公開される関数や実装される特性を減らす)、
- コピー・セマンティクスを持つ型にムーブ・セマンティクスを持たせる、
- より具象的な型を提供することで抽象化し、内部型を隠蔽する、例えば…
pub struct Foo(Bar<T1, T2>);
ここで、Bar
は public なジェネリック型であり、T1
と T2
は内部型です。このモジュールのユーザは Bar
を使って Foo
を実装していることを知るべきではないはずです。しかし、ここで本当に隠しているのは T1
と T2
という型であり、それらが Bar
でどのように使われているかということです。
See also
- the book の Advanced Types
- Haskell でのニュータイプ
- タイプエイリアス
- derive_moreは、ニュータイプに多くの組み込み特性を派生させるためのクレートです。
- RustでのNewtypeパターン
ガード付きRAII
説明
RAIIは「Resource Acquisition is Initialisation」(すごい名称です)の略です。このパターンの本質は、リソースの初期化をオブジェクトのコンストラクタで行い、ファイナライズをデストラクタで行うというものです。RAIIオブジェクトをリソースのガードとして使い、またアクセスが常にガードオブジェクトに仲介されることの保証を型システムに任せることで、このパターンはRustにて拡張されます。
例
ミューテックスのガードは、stdライブラリにあるこのパターンの典型的な例です (これは実際の実装を簡略化したものです) :
use std::ops::Deref;
struct Foo {}
struct Mutex<T> {
// データ: T への参照をここで保持する。
//..
}
struct MutexGuard<'a, T: 'a> {
data: &'a T,
//..
}
// ミューテックスのロックは明示的に行う。
impl<T> Mutex<T> {
fn lock(&self) -> MutexGuard<T> {
// OSのミューテックスをロックする。
//..
// MutexGuardはselfへの参照を保持する。
MutexGuard {
data: self,
//..
}
}
}
// ミューテックスのロックを解除するためのデストラクタ。
impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
// OSのミューテックスのロックを解除する。
//..
}
}
// Derefを実装するということは、MutexGuardをTへのポインタのように扱うことができるということだ。
impl<'a, T> Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self.data
}
}
fn baz(x: Mutex<Foo>) {
let xx = x.lock();
xx.foo(); // fooはFooのメソッド。
// 借用チェッカーは、ガード xx よりも長い Foo への参照を
// 保持できないことを保証する。
// この関数を終了し、xxのデストラクタが実行されるとき、xのロックは解除される。
}
動機形成
リソースの使用後にファイナライズが必要な場合、RAIIを使用してファイナライズを行うことができます。ファイナライズ後にリソースにアクセスすることがエラーとなる場合、このパターンはそのようなエラーを防ぐことに使用できます。
長所
リソースがファイナライズされない、もしくはリソースがファイナライズされた後に使用されてしまう、といったエラーを防ぎます。
議論
RAIIは、リソースが適切に解放またはファイナライズされることを保証するための有用なパターンです。Rustの借用チェッカーを使用することで、ファイナライズ後にリソースを使用することにより生じるエラーを静的に防ぐことが可能です。
借用チェッカーの主な目的は、データへの参照がそのデータ自体より長く生存しないことを保証することです。RAIIガードパターンが機能するのは、ガードオブジェクトが裏のリソースへの参照を保持し、その参照のみを公開するからです。Rustは、ガードが裏のリソースより長く生存しないことを保証するとともに、ガードの仲介するリソースへの参照がガードよりも長く生存しないことを保証します。これがどのように機能するかを見るために、ライフタイムを省略せずに deref
のシグネチャを確かめることが助けになるでしょう:
fn deref<'a>(&'a self) -> &'a T {
//..
}
戻り値であるリソースへの参照は self
と同じライフタイム ('a
) を持ちます。したがって、借用チェッカーは T
への参照のライフタイムが self
のライフタイムよりも短いことを保証します。
Deref
を実装することはこのパターンの核心部分でなく、ガードオブジェクトをより人間工学的に使えるようにするだけであることに注意してください。ガードに get
メソッドを実装しても同じように動作します。
See also
C++ではRAIIは一般的なパターン: cppreference.com、[wikipedia][ウィキペディア]。
Style Guidelines のエントリ (現在は単なるプレースホルダ)。
Strategy (Policy とも)
説明
Strategy デザインパターンは、関心事の分離を可能にするテクニックです。また、依存関係逆転の原則によって、ソフトウェアモジュールを分離することも可能になります。
Strategyパターンの基本的な考え方は、ある特定の問題を解決するアルゴリズムが与えられたとき、抽象的なレベルではアルゴリズムの骨組みだけを定義し、具体的なアルゴリズムの実装は別の部分に分離する、というものです。
この方法では、アルゴリズムを使用するクライアントは、アルゴリズムの共通のワークフローをそのままに、具体的な実装を選択することが可能です。言い換えれば、クラスの抽象的な仕様は派生クラスの具体的な実装に依存せず、具体的な実装が抽象的な仕様に従わなければならない、ということです。これが「依存性逆転」と呼ばれる理由です。
動機形成
私たちは毎月レポートを生成するプロジェクトに取り組んでいるとしましょう。このレポートはさまざまな形式 (ストラテジ)、例えば JSON
や Plain Text
など、で生成される必要があります。しかし、物事は時間の経過とともに変化するもので、将来どんな要求が来るかわかりません。例えば、まったく新しい形式でレポートを生成する必要があるかもしれませんし、あるいは既存のフォーマットの1つを変更するだけかもしれません。
例
この例では、 Formatter
と Report
が不変(または抽象)な部分であり、対して Text
と Json
がストラテジの構造体です。これらのストラテジは Formatter
トレイトを実装する必要があります。
use std::collections::HashMap; type Data = HashMap<String, u32>; trait Formatter { fn format(&self, data: &Data, buf: &mut String); } struct Report; impl Report { // Writeを使うべきだが、エラー処理をサボるためにStringのままにしている。 fn generate<T: Formatter>(g: T, s: &mut String) { // バックエンドの操作... let mut data = HashMap::new(); data.insert("one".to_string(), 1); data.insert("two".to_string(), 2); // レポートの生成 g.format(&data, s); } } struct Text; impl Formatter for Text { fn format(&self, data: &Data, buf: &mut String) { for (k, v) in data { let entry = format!("{k} {v}\n"); buf.push_str(&entry); } } } struct Json; impl Formatter for Json { fn format(&self, data: &Data, buf: &mut String) { buf.push('['); for (k, v) in data.into_iter() { let entry = format!(r#"{{"{}":"{}"}}"#, k, v); buf.push_str(&entry); buf.push(','); } if !data.is_empty() { buf.pop(); // 最後の余分な , を取り除く } buf.push(']'); } } fn main() { let mut s = String::from(""); Report::generate(Text, &mut s); assert!(s.contains("one 1")); assert!(s.contains("two 2")); s.clear(); // 同じバッファを再利用する Report::generate(Json, &mut s); assert!(s.contains(r#"{"one":"1"}"#)); assert!(s.contains(r#"{"two":"2"}"#)); }
長所
主な利点は関心の分離です。例えば、この例の Report
は、 Json
と Text
の具体的な実装については何も知りません。その一方では、出力の実装はデータがどのように前処理され、保存され、フェッチされるのか気にしません。それらが知っていなければならないのは、実装すべき「特定のトレイト」と、結果の処理を行うアルゴリズムの具体的な実装を定義している「そのメソッド」、つまり Formatter
と format(...)
、のみです。
短所
各ストラテジ毎に少なくとも1つのモジュールが実装されることになるため、ストラテジの数だけモジュールの数が増えます。選択できるストラテジーがたくさんある場合、ユーザーはあるストラテジと別のストラテジとの違いを知らなければなりません。
議論
前の例では、すべてのストラテジは1つのファイルに実装されていました。個々のストラテジーを提供する方法には次のようなものがあります:
- 1つのファイルにオールインワン (上例で示したもの、モジュールとして分離されているのと似ている)
- モジュールとして分離、例えば
formatter::json
モジュールとformatter::text
モジュール - コンパイラのfeatureフラグを利用 (例:
json
feature、text
feature) - クレートとして分離、例えば
json
クレート、text
クレートなど
Serde crateは Strategy
パターンが実際に使われている良い例です。Serdeでは、型に Serialize
と Deserialize
を手動で実装することで、シリアライズ動作の フルカスタマイズ が可能です。例えば、 serde_json
と serde_cbor
は似たようなメソッドを公開しているため、これらを簡単に入れ替えることができます。これにより、ヘルパークレート serde_transcode
がより便利で人間工学的なものになります。
しかしながら、Rustでこのパターンを設計するためにトレイトを使うことは必須ではありません。
次の単純化した例は、Rustの closure
を使った Strategy パターンのアイデアを示しています:
struct Adder; impl Adder { pub fn add<F>(x: u8, y: u8, f: F) -> u8 where F: Fn(u8, u8) -> u8, { f(x, y) } } fn main() { let arith_adder = |x, y| x + y; let bool_adder = |x, y| { if x == 1 || y == 1 { 1 } else { 0 } }; let custom_adder = |x, y| 2 * x + y; assert_eq!(9, Adder::add(4, 5, arith_adder)); assert_eq!(0, Adder::add(0, 0, bool_adder)); assert_eq!(5, Adder::add(1, 3, custom_adder)); }
実際、Rust はすでに Options
の map
メソッドでこのアイデアを使っています:
fn main() { let val = Some("Rust"); let len_strategy = |s: &str| s.len(); assert_eq!(4, val.map(len_strategy).unwrap()); let first_byte_strategy = |s: &str| s.bytes().next().unwrap(); assert_eq!(82, val.map(first_byte_strategy).unwrap()); }
See also
- Strategy Pattern
- Dependency Injection
- Policy Based Design
- Strategyパターンを用いたRustでの宇宙アプリケーション用TCPサーバーの実装
Visitor
説明
Visitor は、異種のオブジェクトのコレクションを操作するアルゴリズムをカプセル化します。これにより、データ(またはその主要な振る舞い)を変更する必要なしに、同じデータに対して複数の異なるアルゴリズムを書くことができます。
さらに、Visitor パターンでは、オブジェクトの集合の走査を、各オブジェクトに対し実行される操作から分離することができます。
例
// 訪問するデータ
mod ast {
pub enum Stmt {
Expr(Expr),
Let(Name, Expr),
}
pub struct Name {
value: String,
}
pub enum Expr {
IntLit(i64),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
}
}
// 抽象的な visitor
mod visit {
use ast::*;
pub trait Visitor<T> {
fn visit_name(&mut self, n: &Name) -> T;
fn visit_stmt(&mut self, s: &Stmt) -> T;
fn visit_expr(&mut self, e: &Expr) -> T;
}
}
use ast::*;
use visit::*;
// 具体的な実装の1例 - ASTをコードとして解釈して歩く。
struct Interpreter;
impl Visitor<i64> for Interpreter {
fn visit_name(&mut self, n: &Name) -> i64 {
panic!()
}
fn visit_stmt(&mut self, s: &Stmt) -> i64 {
match *s {
Stmt::Expr(ref e) => self.visit_expr(e),
Stmt::Let(..) => unimplemented!(),
}
}
fn visit_expr(&mut self, e: &Expr) -> i64 {
match *e {
Expr::IntLit(n) => n,
Expr::Add(ref lhs, ref rhs) => self.visit_expr(lhs) + self.visit_expr(rhs),
Expr::Sub(ref lhs, ref rhs) => self.visit_expr(lhs) - self.visit_expr(rhs),
}
}
}
ASTデータを変更することなく、さらなる visitor 、例えば型チェッカーなど、を実装可能です。
動機形成
Visitor パターンは、異種データにアルゴリズムを適用したい場合に便利です。データが同種であれば、イテレータのようなパターンを使うことができます。 (関数型アプローチと比較して) visitor オブジェクトを使用することにより、visitor はステートフルになり、そのためノード間で情報を伝達することができます。
議論
visit_*
メソッドが void を返すのは(例とは異なり)一般的です。この場合、走査コードを分離してアルゴリズム間で共有することが可能です(また、なにもしないデフォルトメソッドを提供することも可能です)。Rustでは、データ各個に対して walk_*
関数を提供するのが一般的です。例えば、
pub fn walk_expr(visitor: &mut Visitor, e: &Expr) {
match *e {
Expr::IntLit(_) => {}
Expr::Add(ref lhs, ref rhs) => {
visitor.visit_expr(lhs);
visitor.visit_expr(rhs);
}
Expr::Sub(ref lhs, ref rhs) => {
visitor.visit_expr(lhs);
visitor.visit_expr(rhs);
}
}
}
他の言語 (例えばJava) では、データに対し同様の責務を担う accept
メソッドがあるのが普通です。
See also
Visitor パターンは、ほとんどのオブジェクト指向言語で一般的なパターンです。
Foldパターンは Visitor に似ていますが、訪問したデータ構造の新しいバージョンを生成する点が異なります。
生成に関するパターン
Wikipediaより:
Design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or in added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation. (オブジェクト生成のメカニズムを扱う、状況に適した方法でオブジェクトを生成しようとするデザインパターン。状況に適した方法でオブジェクト生成の基本形は設計上の問題が発生したり、設計が複雑になったりします。生成に関するデザインパターンは、オブジェクト生成を何らかの方法で制御することで、この問題を解決します。)
Builder
説明
builder ヘルパの呼び出しによりオブジェクトを構築します。
例
#![allow(unused)] fn main() { #[derive(Debug, PartialEq)] pub struct Foo { // 複雑なフィールドがたくさん。 bar: String, } impl Foo { // このメソッドは、ユーザがビルダーを発見しやすくする pub fn builder() -> FooBuilder { FooBuilder::default() } } #[derive(Default)] pub struct FooBuilder { // おそらく任意選択のフィールドがたくさん。 bar: String, } impl FooBuilder { pub fn new(/* ... */) -> FooBuilder { // Foo の最低限必要なフィールドを設定する。 FooBuilder { bar: String::from("X"), } } pub fn name(mut self, bar: String) -> FooBuilder { // ビルダー自身に名前を設定し、ビルダーを値で返す。 self.bar = bar; self } // もしここでビルダーを消費せずに済むのであれば、これは利点となる。 // つまり、FooBuilderをテンプレートとして使って、 // 複数のFooを構築することができる。 pub fn build(self) -> Foo { // FooBuilder から Foo を作成する。このとき FooBuilder のすべての設定を // Foo に適用する。 Foo { bar: self.bar } } } #[test] fn builder_test() { let foo = Foo { bar: String::from("Y"), }; let foo_from_builder: Foo = FooBuilder::new().name(String::from("Y")).build(); assert_eq!(foo, foo_from_builder); } }
動機形成
多くのコンストラクタが必要な状況や、インスタンス構築時に副作用がある状況に便利です。
長所
構築のためのメソッドを他のメソッドから分離します。
コンストラクタの増殖を防ぎます。
ワンライナーでの初期化にも、より複雑な構築にも使用できます。
短所
構造体オブジェクトを直接作成したり、単純なコンストラクタ関数を使用するよりも複雑です。
議論
Rustにはオーバーロードがないため、他の多くの言語と比較して、このパターンが頻繁に登場します(またより単純なオブジェクトに適用されます)。1つの名前を持つメソッドは1つのみであることから、Rust にて複数のコンストラクタを定義することは、C++ や Java などと比べてあまり良いことではありません。
このパターンは、builder オブジェクトが単なる builder としてではなく、それ自体として有用である場合によく使われます。例えばstd::process::Command
は Child
(プロセス) のビルダーです。これらの場合、 T
と TBuilder
の命名パターンは使用されていません。
例ではbuilderを値として引数に受け、return しています。可変参照として引数に受け、return する形式が、多くの場合、より人間工学的(かつ効率的)です。借用チェッカーはこれを自然に動作させます。このアプローチには下記のようなコードを書けるようになるメリットがあります
let mut fb = FooBuilder::new();
fb.a();
fb.b();
let f = fb.build();
FooBuilder::new().a().b().build()
のスタイルと同様です。
See also
- Style Guideでの記述
- derive_builderは、このパターンを自動的に実装するためのクレートです。ボイラーテンプレートを省けます。
- コンストラクタ パターン 構築がより単純な場合はこちら。
- Builder pattern (wikipedia)
- 複雑な値の構築
Fold
説明
データのコレクション内の各項目に対してアルゴリズムを実行し新しい項目を作成します。このようにして、全く新しいコレクションを作成します。
この語源は私にはよくわからないところがあります。「fold」と「folder」という用語は、Rustコンパイラで使われています。通常の感覚では fold というより map が近いように思います。詳しくは下記の議論を参照してください。
例
// foldされるデータ、単純なAST。
mod ast {
pub enum Stmt {
Expr(Box<Expr>),
Let(Box<Name>, Box<Expr>),
}
pub struct Name {
value: String,
}
pub enum Expr {
IntLit(i64),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
}
}
// folder の抽象
mod fold {
use ast::*;
pub trait Folder {
// リーフノードは単にノード自身を返す。多くの状況では、
// 内部ノードについても同様にすることが可能。
fn fold_name(&mut self, n: Box<Name>) -> Box<Name> { n }
// 子ノードを fold し新たな内部ノードを作成する。
fn fold_stmt(&mut self, s: Box<Stmt>) -> Box<Stmt> {
match *s {
Stmt::Expr(e) => Box::new(Stmt::Expr(self.fold_expr(e))),
Stmt::Let(n, e) => Box::new(Stmt::Let(self.fold_name(n), self.fold_expr(e))),
}
}
fn fold_expr(&mut self, e: Box<Expr>) -> Box<Expr> { ... }
}
}
use fold::*;
use ast::*;
// 具体的な実装の例 - すべての Name を 'foo' にリネーム。
struct Renamer;
impl Folder for Renamer {
fn fold_name(&mut self, n: Box<Name>) -> Box<Name> {
Box::new(Name { value: "foo".to_owned() })
}
// 他のノードではデフォルトのメソッドを使用する。
}
ASTに対して Renamer
を実行した結果は、すべての名前が foo
に変更されたことを除いて元のASTとまったく同一の、新しいASTになります。実際の folder では構造体のノード間で何らかの状態を保持する可能性があります。
folder は、あるデータ構造を別の(しかし通常は類似した)データ構造にマッピングするものとして定義することもできます。例えば、ASTをHIRツリーに折り畳むことができます (HIRは high-level intermediate representation (高水準中間表現) の略です)。
動機形成
データ構造内の各ノードに対して何らかの操作を実行することで、データ構造をマップしたい場合がよくあります。これは、単純なデータ構造に対する単純な操作であれば、 Iterator::map
を使って行えます。より複雑な操作、例えば前のノードが後のノードの操作に影響を与えるような操作や、データ構造に対するイテレーションが自明でないような操作には、foldパターンを使うことがより適切です。
Visitor パターンと同様に Fold パターンでも、データ構造の走査を各ノードに対して行われる操作から分離することができます。
議論
このような形でデータ構造をマッピングすることは、関数型言語では一般的です。OO言語では、データ構造をその場で書き換えることがより一般的でしょう。Rustでは、主に不変性を好むため、「関数型」アプローチが一般的です。古いデータ構造を書き換えるのではなく、新しいデータ構造を使用することで、ほとんどの状況でコードに関する推論が容易になります。
効率と再利用性のトレードオフは、fold_*
メソッドでノードを受け入れる方法を変更することで調整できます。
上の例では Box
ポインタを通して操作を行っています。これらは自身のデータを排他的に所有するため、データ構造の元々の値が再利用不可能になります。その一方で、あるノードに変更が無いのであれば、これをそのまま再利用することができ、非常に効率的です。
これがもしも、借用した参照を通して操作を行っていたのであれば、元のデータ構造は再利用可能です。しかし、あるノードに変更が無い場合でも、そのノードをクローンする必要があります。これはコストがかかるものになりえます。
参照カウント付きポインタを使用すると両方の利点が得られます。元のデータ構造は再利用可能で、かつ変更の無いノードをクローンする必要がないためです。しかしながら、これらは人間工学的に使いにくいところがあり、またデータ構造をミュータブルにできないことを意味します。
See also
イテレータには fold
メソッドがありますが、これはデータ構造を新しいデータ構造に fold するのではなく、1つの値に畳み込みます。イテレータの map
が、この Fold パターンには似ています。
他の言語では、fold は、この Fold パターンではなく、通常Rustのイテレータにおける意味 (fold
メソッド) で使われます。関数型言語の中には、データ構造に対して柔軟なマップを行うための強力な構成要素を持つものがあります。
Visitorパターンは Fold と密接に関連しています。この2つのパターンは、データ構造を巡りながら各ノードに対して操作を行う、という概念を共有しています。しかし、Visitor は新しいデータ構造を生成したり、古いデータ構造を消費したりはしません。
構造に関するパターン
Wikipediaより:
Design patterns that ease the design by identifying a simple way to realize relationships among entities. (エンティティ間の関係を表現するシンプルな方法を特定することで、設計を容易にするデザインパターン。)
個別借用のための構造体の分解
説明
大きな構造体では、借用チェッカーにて問題が発生することがあります - フィールドを個別に借用することが可能なのですが、同時に構造体全体が使用されてしまうことになり、別の利用を妨げることがあります。解決策としては、構造体を複数の小さな構造体に分解することです。それから、これらを元の構造体にまとめます。そうすれば、各構造体を個別に借用でき、より柔軟な操作が可能になります。
これは、別の形でより良い設計につながることがあります: このデザインパターンを適用することにより、より小さな機能の単位の存在が明らかになることがあります。
例
以下は、借用チェッカーが構造体の利用を妨げ失敗させる、作為的な例です:
struct Database { connection_string: String, timeout: u32, pool_size: u32, } fn print_database(database: &Database) { println!("Connection string: {}", database.connection_string); println!("Timeout: {}", database.timeout); println!("Pool size: {}", database.pool_size); } fn main() { let mut db = Database { connection_string: "initial string".to_string(), timeout: 30, pool_size: 100, }; let connection_string = &mut db.connection_string; print_database(&db); // `db` の不変借用が発生 // *connection_string = "new string".to_string(); // ここで可変借用を使用 }
当デザインパターンを適用し、Database
を3つの小さな構造体にリファクタリングします。これにより借用チェッカーの問題を解決します:
// Database is now composed of three structs - ConnectionString, Timeout and PoolSize. // Let's decompose it into smaller structs #[derive(Debug, Clone)] struct ConnectionString(String); #[derive(Debug, Clone, Copy)] struct Timeout(u32); #[derive(Debug, Clone, Copy)] struct PoolSize(u32); // We then compose these smaller structs back into `Database` struct Database { connection_string: ConnectionString, timeout: Timeout, pool_size: PoolSize, } // print_database can then take ConnectionString, Timeout and Poolsize struct instead fn print_database(connection_str: ConnectionString, timeout: Timeout, pool_size: PoolSize) { println!("Connection string: {connection_str:?}"); println!("Timeout: {timeout:?}"); println!("Pool size: {pool_size:?}"); } fn main() { // 以下の3つの構造体でデータベースを初期化する。 let mut db = Database { connection_string: ConnectionString("localhost".to_string()), timeout: Timeout(30), pool_size: PoolSize(100), }; let connection_string = &mut db.connection_string; print_database(connection_string.clone(), db.timeout, db.pool_size); *connection_string = ConnectionString("new string".to_string()); }
動機形成
このパターンは特に便利なのは、構造体に個別に借用したいフィールドがたくさんある場合です。これにより、最終的により柔軟な操作が可能になります。
長所
構造体の分解は借用チェッカーの制限を回避するのに利用できます。またより良い設計を生み出すこともあります。
短所
冗長なコードにつながる可能性があります。またときに、より小さな構造体が良い抽象でなく、より悪い設計になってしまうことがあります。これはおそらく「コードの臭い」であり、プログラムを何らかの方法でリファクタすべきことを示しています。
議論
このパターンは、借用チェッカーを持たない言語では必要ありません。その意味ではRust独自のものです。しかし、機能の単位を小さくすることがクリーンなコードにつながることは、ソフトウェア工学の原則として、言語とは関係なく広く認められています。
このパターンは、Rustの借用チェッカーがフィールドを互いに独立して借用できることに依存しています。この例では、借用チェッカーは a.b
と a.c
が別個のものであり、独立して借用できることを知っており、a
のすべてを借用しようとはしません。そうでなければ、このパターンは役に立たないところでした。
小さなクレートを選好する
説明
1つのことをよく熟す小さなクレートであることを選好しましょう。
cargo と crates.io は、サードパーティーライブラリの追加を、CやC++よりもはるかに簡単なものにします。また、crates.io上のパッケージは公開後に編集・削除不可能なため、現在機能しているビルドは将来も機能し続けるはずです。私たちはこのツールを活用し、より小さく、細かな依存関係を使うべきです。
長所
- 小さなクレートは理解しやすく、よりモジュール化されたコードを促進します。
- クレートはプロジェクト間でのコードの再利用を可能にします。例えば、
url
クレートは Servo ブラウザエンジンの一部として開発されましたが、その後プロジェクト外で広く使用されるようになりました。 - Rustのコンパイル単位はクレートであるため、プロジェクトを複数のクレートに分割することで、より多くのコードを並列にビルドできるようになります。
短所
- これはプロジェクトが競合するバージョンの複数のクレートに依存しているとき、「依存性地獄」につながる可能性があります。例えば、
url
クレートはバージョン 1.0 と 0.5 があります。url:1.0
のUrl
とurl:0.5
のUrl
は異なる型であるため、url:0.5
を使用するHTTPクライアントは、url:1.0
を使用するWebスクレイパーからのUrl
を受け付けません。 - crates.io のパッケージはキュレーションされていません。クレートには作りが甘かったり、ドキュメントが助けにならないものであったり、あからさまに悪意のあるものもあります。
- コンパイラはデフォルトでリンク時最適化(LTO)を行わないので、2つの小さなクレートは1つの大きなクレートよりも最適化されていないかもしれません。
例
url
crateはURLを扱うツールを提供します。
num_cpus
crate はマシンのCPU数を問い合わせる関数を提供します。
ref_slice
crate は &T
を &[T]
に変換する関数を提供します(歴史的な例)。
See also
安全でないものを小さなモジュールに閉じ込める
説明
unsafe
なコードがある場合、安全でないコードの上に最小限の安全なインタフェースを構築するために、必要な不変性を維持できる可能な限り小さなモジュールを作りましょう。これを安全なコードのみ含む大きなモジュールに組み込み、人間工学的なインタフェースを提供してください。外側のモジュールには unsafe なコードを呼び出す unsafe な関数やメソッドを含めてもよいことに注意してください。ユーザはこれを使用して、速度の利点を得ることができます。
長所
- これにより、精査する必要のある unsafe なコードが制限されます
- 内側のモジュールによる保証に頼ることで、外側のモジュールを書くことがずっと簡単になります
短所
- 適切なインタフェースを見つけるのが難しい場合もあります。
- 抽象化が非効率をもたらすかもしれません。
例
toolshed
クレートは、安全でない操作をサブモジュールにとじ込めることで、ユーザーに安全なインターフェイスを提供します。std
のString
クラスは、その中身が妥当なUTF-8であるという不変性を追加したVec<u8>
のラッパーです。String
に対する操作はこの振る舞いを保証します。しかし、ユーザが、その内容の妥当性を保証する責任を持つ前提で、String
を作成するunsafe
メソッドを利用する選択肢があります。
See also
FFI に関するパターン
FFIコードを書くことは、それ自体が1つの大きな課程であると言えます。しかしながら、ポインタの役割を担い、unsafe Rustの経験の浅いユーザーが陥る罠を回避する、いくつかのイディオムがあります。
このセクションでは、FFIを行う際に役立つデザインパターンを紹介します。
-
オブジェクトベースのAPI は優れたメモリ安全性を設計し、何が安全で何が安全でないかの境界を明確にします
-
型のラッパーへの統合 - 複数のRust型を不透明な「オブジェクト」にまとめます
オブジェクトベースのAPI
説明
Rustにて他の言語へ公開するAPIを設計する場合、通常のRustのAPI設計にそむく重要な設計原則がいくつかあります:
- カプセル化された型はすべて、Rustが 所有 し、ユーザーが 管理 し、また 不透過的 であるべきです。
- すべてのトランザクションデータ型は、ユーザが 所有 し、 透過的 であるべきです。
- すべてのライブラリの操作は、カプセル化された型に作用する関数であるべきです。
- すべてのライブラリの操作は、構造に基づく型ではなく、provenance(由来)/ライフタイム に基づく型にカプセル化されるべきです。
動機形成
Rustには、他の言語へのFFIサポートが組み込まれています。これは、様々なABIを通してC互換のAPIを提供する方法を、クレート作者に提供することで実現しています (このことは当実践には重要ではありませんが)。
うまく設計されたRust FFIは、C言語のAPI設計の原則に従いつつ、Rustの設計を可能な限り損なわないようにします。言語間のAPIには3つのゴールがあります:
- ターゲット言語で利用しやすいようにします。
- APIがRust側の内部的な不安定性を規定することを可能な限り避けましょう。
- メモリの不安定性やRustの
undefined behavior
の可能性を可能な限り小さく保ちましょう。
Rustコードは、外部言語のメモリ安全性を一定以上信頼しなければなりません。しかし、Rust側の unsafe
コードのすべてはバグを発生させたり、 undefined behaviour
を悪化させるきっかけとなりえます。
例えば、ポインタの由来が不適切である場合、それは無効なメモリアクセスが原因のセグメンテーション違反となりえます。しかし、それが unsafe コードによって操作された場合、本格的なヒープ破壊を起こす可能性があります。
オブジェクトベースのAPI設計は、メモリ安全性に優れた“くさび”を記述することを可能にし、また unsafe
なものと安全なものの境界を明確にするものです。
コードの例
POSIX標準では、DBMとして知られるオンファイル・データベースにアクセスするためのAPIを定義しています。これは「オブジェクトベース」APIの優れた例です。
以下はC言語による定義です。FFIに携わっている人には読みやすいと思います。以下の解説は、微妙な点を理解し損ねた人のために役立つはずです。
struct DBM;
typedef struct { void *dptr, size_t dsize } datum;
int dbm_clearerr(DBM *);
void dbm_close(DBM *);
int dbm_delete(DBM *, datum);
int dbm_error(DBM *);
datum dbm_fetch(DBM *, datum);
datum dbm_firstkey(DBM *);
datum dbm_nextkey(DBM *);
DBM *dbm_open(const char *, int, mode_t);
int dbm_store(DBM *, datum, datum, int);
このAPIは2つの型を定義しています: DBM
と datum
です。
DBM
型は上述した「カプセル化された」型と呼ばれるものです。DBM
型は内部状態を持つように設計されており、ライブラリの操作のエントリポイントとして振る舞います。
サイズやレイアウトを知らないので、ユーザは DBM
直接作成することができません。その代わりに、 dbm_open
を呼び出す必要があります。そしてこれは DBMへのポインタ を与えてくれるだけです。
これはRust的な感覚で言えば、ライブラリがすべての DBM
を「所有」している、ということになります。サイズ不明の内部状態は、ユーザ側ではなく、ライブラリが管理するメモリに保持されます。ユーザが行えるのは、 DBM
のライフサイクルを open
と close
によって管理し、他の関数によって操作することに限られます。
datum
型は上述した 「トランザクション」型と呼ばれるものです。これはライブラリとユーザの間の情報交換を容易にするために設計されています。
データベースは、あらかじめ長さや意味の定義されていない「非構造化データ」を保存するように設計されています。その結果、datum
はCにおいてRust のスライスに相当するもの - バイトの列であり、その数をカウントされたもの - です。主な相違点は、これには型情報がないことです。 void
はそれを示唆しています。
このヘッダはライブラリの視点から記述されていることに留意してください。ユーザは、サイズが既知である何らかの型を使用しているはずです。しかし、ライブラリはそれについて関知しません。またCのキャストの規則では、あらゆる型を void
にキャストできます。
前述したように、この型はユーザにとって 透過的 です。しかし同時に、この型はユーザによって 所有 されます。このことは、内部にあるポインタによって、微妙な派生があります。問題は、そのポインタが指すメモリは誰が所有するのか?ということです。
最高のメモリ安全性のための答えは「ユーザ」です。しかし、値の取得を行うような場合、ユーザは (値の長さを知らないので) 正しくアロケートする方法を知りません。この場合ライブラリのコードには、ユーザがアクセスできるヒープを使用 - Cの malloc
や free
のように - することが期待され、さらにはRust的な意味での 所有権移転 を行うことが期待されます。
まったく観念的ですが、これこそがC言語におけるポインタの意味です。これはRustにおける「ユーザ定義のライフタイム」と同じ意味です。このライブラリのユーザは、正しく使うためにドキュメントを読む必要があります。ドキュメントは、ユーザが間違った使い方をした場合に、大小さまざまな悪い結果をもたらす可能性があることについて記載されています。それらを最小限にすることが、このベストプラクティスの目的であり、 透過的であるものすべての所有権を移転すること が鍵なのです。
長所
これにより、ユーザが守らなければならないメモリ安全性保証を、比較的少数に抑えます:
dbm_open
が返していないポインタを使って関数を呼び出さないでください (無効なアクセスや破損)。- close後のポインタを使って関数を呼び出さないでください (解放後参照( Use-After-Free ))。
datum
のdptr
はNULL
であるか、または適切な長さの有効なメモリスライスを指している必要があります。
さらに、これは、ポインタの由来に関する多くの問題を回避することができます。その理由を理解するために、キーの反復という代替案について少し深く検討してみましょう。
Rustはイテレータでよく知られています。イテレータを実装する場合、プログラマは、所有型に対しライフタイムが制限された別の型を作成し、 Iterator
トレイトを実装します。
以下は DBM
に対して行われ得る Rust での反復処理の方法です:
struct Dbm { ... }
impl Dbm {
/* ... */
pub fn keys<'it>(&'it self) -> DbmKeysIter<'it> { ... }
/* ... */
}
struct DbmKeysIter<'it> {
owner: &'it Dbm,
}
impl<'it> Iterator for DbmKeysIter<'it> { ... }
これはクリーンでイディオム的で安全です。Rustの保障によるものです。しかし、APIをそのままに翻訳したものがどのようになるかを考えてみましょう:
#[no_mangle]
pub extern "C" fn dbm_iter_new(owner: *const Dbm) -> *mut DbmKeysIter {
// このAPIは悪い例です! 実際のアプリケーションではオブジェクトベースの設計を使用してください。
}
#[no_mangle]
pub extern "C" fn dbm_iter_next(
iter: *mut DbmKeysIter,
key_out: *const datum
) -> libc::c_int {
// このAPIは悪い例です! 実際のアプリケーションではオブジェクトベースの設計を使用してください。
}
#[no_mangle]
pub extern "C" fn dbm_iter_del(*mut DbmKeysIter) {
// このAPIは悪い例です! 実際のアプリケーションではオブジェクトベースの設計を使用してください。
}
このAPIは重要な情報を失っています。それは、イテレータのライフタイムはそれを所有する Dbm
オブジェクトのライフタイムを超えてはいけない、ということです。ライブラリのユーザは反復対象のデータよりも長生きするイテレータを使用してしまう可能性があります。その結果、初期化されていないメモリを読み込むことになります。
C言語で書かれたこの例には後述するバグがあります:
int count_key_sizes(DBM *db) {
// この関数は使わないでください。微妙な、しかし重大なバグがあります!
datum key;
int len = 0;
if (!dbm_iter_new(db)) {
dbm_close(db);
return -1;
}
int l;
while ((l = dbm_iter_next(owner, &key)) >= 0) { // エラーは -1 で示す
free(key.dptr);
len += key.dsize;
if (l == 0) { // 反復終了
dbm_close(owner);
}
}
if l >= 0 {
return -1;
} else {
return len;
}
}
このバグは古典的なものです。イテレータが反復完了を返したときに起こります:
- ループの条件式は
l
を 0 に設定し、0 >= 0
なのでループに入ります。 - len が計上されます。この場合 0 です。
-
- if文は真なので、データベースは閉じられます。ここで break文があるはずでした。
- ループの条件式が再度実行され、closeされたオブジェクトに対して
next
が呼び出されてしまいます。
このバグで最悪なことはなんでしょう?Rustの実装が慎重なものであれば、このコードはほとんどの場合動いてしまいます!もし Dbm
オブジェクトのメモリがすぐに再利用されなければ、内部チェックはほぼ確実に失敗し、イテレータはエラーを示す -1
を返します。しかし、時にはセグメンテーションフォールトを引き起こしたり、さらに悪いことにわけのわからないメモリ破壊を引き起こすこともあります!
Rust側はこのどれも避けようがありません。Rust側から見れば、オブジェクトをヒープに置き、ポインタを返し、そのライフタイムの制御を任せたことになります。Cコードが「うまくやる」しかないのです。
プログラマはAPIドキュメントを読み、理解しなければなりません。C言語では当然のことだと考える人もいますが、優れたAPI設計はこのリスクを軽減することができます。DBM
の POSIX API ではイテレータの 所有権をその親に統合する ことによりこれを実現しています:
datum dbm_firstkey(DBM *);
datum dbm_nextkey(DBM *);
こうして、すべてのライフタイムが一つにまとめられ、このような安全性の欠如が予防されています。
短所
しかしながら、この設計の選択にはいくつかの欠点があり、十分に検討すべきです。
まず、API自体の表現力が低下します。POSIX DBMでは、1つのオブジェクトにつきイテレータは1つしかありません。また呼び出しにより状態が変更されます。これは、安全であるとはいえ、ほとんどすべての言語のイテレータよりもはるかに制限的なものです。おそらく他の関連オブジェクトの、そのライフタイムがそれほど階層的でないもの、については、この制限は安全性よりもコストになります。
次に、APIの部品との関係によっては、設計に多大な労力が必要になるかもしれません。より簡単な設計点の多くには、他のパターンが関係しています:
-
ラッパーへの型の統合は、複数のRustタイプを不透明な「オブジェクト」にまとめます。
-
FFI のエラー処理 では整数コードと番兵の戻り値 (
NULL
ポインタなど) によるエラー処理について説明しています。 -
しかしながら、すべてのAPIがこの方法でできるわけではありません。 利用者が誰であるか、プログラマの最善の判断次第です。
しかしながら、すべてのAPIがこの方法でできるわけではありません。利用者が誰であるかは、プログラマの最善の判断次第です。
ラッパーへの型の統合
説明
このパターンは、メモリ安全性の損なわれる表面面積を最小化しつつ、関連する複数の型を優雅に扱えるように設計されています。
One of the cornerstones of Rust’s aliasing rules is lifetimes. This ensures that many patterns of access between types can be memory safe, data race safety included.
しかし、Rustの型が他の言語にエクスポートされる場合、通常はポインタに変換されます。Rustでは、ポインタは「ポインタが指すオブジェクトのライフタイムを、ユーザが管理する」ことを意味します。メモリの安全性が損なわれないようにすることは、ユーザの責務です。
Some level of trust in the user code is thus required, notably around use-after-free which Rust can do nothing about. However, some API designs place higher burdens than others on the code written in the other language.
最もリスクの低いAPIは「統合ラッパー」です。RustのAPIをクリーンに保ちながら、オブジェクトとの間のやりとりはすべて「ラッパー型」に畳み込むものです。
コードの例
これを理解するために、エクスポートするAPIの典型的な例として「コレクションのイテレーション」を見てみましょう。
このAPIは以下の通りです:
- イテレータは
first_key
で初期化されます。 next_key
を呼び出すたびにイテレータが進みます。- イテレータが末尾にある場合に
next_key
を呼び出しても何も起こりません。 - 上述の通り、イテレータはコレクションに「ラップ」されます(Rust ネイティブ のAPI とは異なる部分です)。
イテレータが nth()
を効率的に実装しているならば、関数の各呼び出しに対してイテレータを短命なものとすることが可能です:
struct MySetWrapper {
myset: MySet,
iter_next: usize,
}
impl MySetWrapper {
pub fn first_key(&mut self) -> Option<&Key> {
self.iter_next = 0;
self.next_key()
}
pub fn next_key(&mut self) -> Option<&Key> {
if let Some(next) = self.myset.keys().nth(self.iter_next) {
self.iter_next += 1;
Some(next)
} else {
None
}
}
}
結果、ラッパーはシンプルで、 unsafe
コードを含みません。
長所
これにより、APIをより安全に使用できるようになり、型間のライフタイムの問題を避けることができます。オブジェクトベースのAPI には、この利点と、これにより回避できる落とし穴についてさらなる記載があります。
短所
多くの場合、型のラッピングは非常に難しいものです。事を簡単にするために Rust API を妥協することになるかもしれません。
例として、nth()
の実装が効率的でないイテレータを考えてみましょう。オブジェクトに内部に反復を処理させる特殊なロジックや、FFI の API からのみ使用される別のアクセスパターンを効率的にサポートすること、これらには間違いなく価値があります。
イテレータのラップに挑戦(そして失敗)
どのようなタイプのイテレータでもAPIに正しくラップするためには、ラッパーはCバージョンのコードが行うようなことを行う必要があります。つまりイテレータのライフタイムを消去し、それを手動で管理することです。
言うまでもなく、これは 信じられないほど 難しいことです。
ここで、 落とし穴をたった ひとつ 、ご紹介しましょう。
最初のバージョンの MySetWrapper
は次のようになります:
struct MySetWrapper {
myset: MySet,
iter_next: usize,
// Box<KeysIter + 'self> を transmute して作成
iterator: Option<NonNull<KeysIter<'static>>>,
}
ライフタイムを延長するために行う transmute
に、それを隠すためのポインタ、もうすでに醜いでしょう。しかし、さらに悪くなります: 他のあらゆる操作がRust の undefinded behaviour
を引き起こします 。
ラッパー内の MySet
は、イテレーション中に他の関数によって操作される可能性があります。例えば、イテレーション中に新しい値を格納するなどです。APIはこれを禁じておらず、実際のところ同様のCライブラリの中にはこれを想定しているものもあります。
myset_store
の単純な実装は次のようになります:
pub mod unsafe_module {
// モジュールの他のコンテンツ
pub fn myset_store(myset: *mut MySetWrapper, key: datum, value: datum) -> libc::c_int {
// このコードを使用しないでください。不具合を実証することは危険です。
let myset: &mut MySet = unsafe {
// SAFETY: あぁ・・・ここで UB が発生します!
&mut (*myset).myset
};
/* ...キーと値のデータをチェックし、キャスト... */
match myset.store(casted_key, casted_value) {
Ok(_) => 0,
Err(e) => e.into(),
}
}
}
この関数が呼ばれたときにイテレータが存在していた場合、Rustののエイリアシングルールに違反したことになります。Rustによると、このブロック内の可変参照はオブジェクトへの 排他的 アクセスでなければなりません。イテレータが単に存在しているだけで、排他的ではなくなります。よって undefined behaviour
が発生します! 1
これを避けるには、可変参照が本当に排他的であることを保証する方法を持たなければなりません。これは基本的に、イテレータの共有参照が存在する間にそれを消去し、後に再構築することを意味します。たいていの場合、それでもCのバージョンより効率は悪くなります。
Cではどうすればもっと効率的にできるのか?疑問に思う人もいるでしょう。答えは・・・ズルすることです。Rustのエイリアシングルールが問題なのであり、Cではそのポインタについてルールを無視します。その代わりに、マニュアルで特定の(もしくはすべての)状況にて「スレッドセーフではない」と宣言されているコードをよく見かけます。実際にGNU Cライブラリには、並列動作に特化した用語集があります!
むしろRustは、安全性とCコードでは達成できない最適化の両方を実現するために、すべてを常にメモリセーフにします。特定のショートカットへのアクセスを拒否されることは、Rustプログラマが支払う価値のある代償です。
頭を悩ませているCプログラマーのために言っておくと、UBを引き起こすこのコード中の間に、イテレータを読み出す必要はありません。また排他的ルールはコンパイラの一部の最適化を有効にしますが、これはイテレータの共有参照からみて一貫性のない現象(例えば、スタックの漏洩や、効率化のための命令の並び替え)を引き起こすことがあります。このような現象は、 可変参照を生成した後は いつでも 発生する可能性があります。
アンチパターン
アンチパターンとは「通常、効果がなく、大きな逆効果になるリスクのある、頻繁に見られる問題のあること」に対するソリューションです。問題を解決する方法を知ることと同じくらい価値があるのは、問題を解決しない方法を知ることです。アンチパターンは、デザインパターンに対して、考慮すべき素晴らしい反例を与えてくれます。アンチパターンはコードに限ったことではありません。例えば、プロセスもアンチパターンになりえます。
借用チェッカーを満足させるためのクローン
説明
借用チェッカーは、Rustユーザが安全でないコードを開発することを、次のことを確保することで防ぎます: 1つだけの可変参照が存在するか、もしくは幾つでもよいがすべて不変である参照が存在すること。もし書かれたコードがこれらの条件を満たさない場合に、コンパイルエラーを解決するために開発者が変数のクローンを作成したとき、このアンチパターンが発生します。
例
#![allow(unused)] fn main() { // 変数を定義 let mut x = 5; // `x` を借用 -- ただし、先だってクローンする let y = &mut (x.clone()); // 2行前の x.clone() 無しでは、xが借用されているためこの行がコンパイルエラーになります。 // x.clone() により、 x は借用されていないため、この行が動作します。 println!("{x}"); // Rust がこれを消去する形の最適化を防ぐため、何らかの操作をします。 *y += 1; }
動機形成
特に初心者の方にとっては、借入チェッカーによるややこしい問題を、このパターンを使って解決したくなるものです。しかし、ここには深刻な結果があります。.clone()
を使うと、データのコピーが作成されます。この2つの間の変更は同期されません – 完全に別の2つの変数が存在することと同じです。
特別なケースがあります – Rc<T>
はクローンをインテリジェントに扱うように設計されています。内部的にデータのコピーを1つだけ持っており、クローンを作成しても参照だけをクローンします。
また、ヒープに確保されたT型の値に対する共有所有権を提供する Arc<T>
もあります。Arc
に対して .clone()
を呼び出すと、新しい Arc
インスタンスが生成されますが、このインスタンスは元とヒープの同じ値を指しています。また同時に、参照カウンタがインクリメントされます。
一般に、クローンはその影響を十全に理解し、意図的に行われるべきものです。もしも借用チェッカーのエラーを消すためにクローンが使われてるなら、それはこのアンチパターンの使われている可能性を示す兆候と言えます。
.clone()
は悪しきパターンの表れですが、ときに 非効率なコードを書いてもよい こともあります。次のようなケースです:
- 開発者が所有権に慣れていない
- コードの速度やメモリに大きな制約がない場合 (ハッカソンプロジェクトやプロトタイプなど)
- 借用チェッカーを満足させることが本当に複雑で難しく、パフォーマンスよりも読みやすさを最適化したい
不必要なクローンが疑われる場合、Rust Book の所有権の章 を十分に理解した上で、クローンが必要かどうかを判断してください。
また、プロジェクト内で常に cargo clippy
を実行するようにしてください。これは1、 2、 3、 4 のような clone()
が不要なケースをいくつか検出します。
See also
- 変更するenum値の所有された値を維持する
mem::{take(_), replace(_)}
Rc<T>
のドキュメント ( .clone() をインテリジェントに扱う)Arc<T>
のドキュメント (スレッドセーフな参照カウンタ付きポインタ)- Rust における所有権のトリック
#![deny(warnings)]
説明
善意のあるcrate作成者は、自分たちのコードが警告なしでビルドされることを保証したいと考えています。そこで、彼らはcrateのルートに次のようなアノテーションを付けます:
例
#![allow(unused)] #![deny(warnings)] fn main() { // すべてちゃんとしている。 }
長所
これは簡単で、もし何か間違いがあるならビルドが止まります。
欠点
コンパイラが警告ありでビルドすることを禁止することにより、Rustの有名な安定性から、crate作成者が切り離されてしまうことになります。時に、新しい機能や古い問題ある機能により、物事の進め方を変える必要がでてくることがあります。このような場合に、lintは一定の猶予期間 warn
し、その後に deny
に切り替えるように記述されます。
例えば、ある型が同じメソッドを持つ2つのimpl
を持つことができることが発見されました。これは悪いアイデアだと判断されました。しかしスムーズに移行するために、overlapping-inherent-impls
というlintが導入を導入し、将来のリリースで完全なエラーとされる前に、この事実につまずいた人に警告を与えるようにしました。
また、APIが非推奨になることもあります。この場合、それを使用していると、今まで無かった警告が出るようになります。
これらすべてが、何かが変わるたびにビルドを壊してしまう可能性を持っています。
さらに、追加のlintを提供するcrate([rust-clippy]など)は、このアノテーションを削除しない限り使用できなくなりました。これは[--cap-lints]で緩和されます。コマンドライン引数に --cap-lints=warn
を指定すると、すべての deny
lint エラーを警告に変えます。
代替案
この問題に対処する方法は2つあります。一つは、コードからビルド設定を切り離すことです。もう一つは、denyしたい lint を明示的に指定することです。
次のコマンドラインは、すべての警告を deny
に設定してビルドを行います:
RUSTFLAGS="-D warnings" cargo build
これは個々の開発者が(あるいはTravisのようなCIツールでも。ただし変更があったときビルドが壊れる可能性に注意は必要)コードに変更を加えずに行えます。
あるいは、コード中で deny
したいlintを指定することもできます。以下は、(たぶん) 安全にdenyできる警告リントのリストです (Rustc 1.48.0 時点):
#![deny(
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
さらに、以下のallow
されたリントをdeny
するのはよい考えでしょう:
#![deny(
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
unused_extern_crates,
unused_import_braces,
unused_qualifications,
unused_results
)]
また、missing-copy-implementations
をリストに加えたい人もいるでしょう。
deprecated
のlintを明示的に追加していないことに注意してください。将来的に非推奨のAPIが増えることは確実だからです。
See also
- すべての crippy lint
- deprecated属性 のドキュメント
rustc -W help
と入力するとあなたのシステム上の lint のリストが表示されます。またrustc --help
と入力すると一般的なオプションのリストが表示されます- [rust-clippy] はより良い Rust コードのための lint のコレクションです
Deref
ポリモーフィズム
説明
構造体間の継承をエミュレートする目的での Deref
トレイトの悪用。これによりメソッドを再利用しようとすること。
例
JavaのようなOO言語からの、次のような一般的なパターンをエミュレートしたいことがあります:
class Foo {
void m() { ... }
}
class Bar extends Foo {}
public static void main(String[] args) {
Bar b = new Bar();
b.m();
}
deref ポリモーフィズムのアンチパターンを使い、これを行います:
use std::ops::Deref; struct Foo {} impl Foo { fn m(&self) { //.. } } struct Bar { f: Foo, } impl Deref for Bar { type Target = Foo; fn deref(&self) -> &Foo { &self.f } } fn main() { let b = Bar { f: Foo {} }; b.m(); }
Rustには構造体の継承はありません。代わりにコンポジションを使用し、 Foo
のインスタンスを Bar
の持ち物にします (フィールドは値なのでインラインに保持されます。したがってフィールドがあるのであれば、それらはJavaのバージョンと同じメモリレイアウトとなるでしょう (おそらくは。確実にそうしたいなら #[repr(C)]
を使うべきです))。
メソッドコールを機能させるために、Bar
に対して Foo
をターゲット(Target)として Deref
を実装します (埋め込まれた Foo
フィールドを返します) 。これは (例えば *
を使い) Bar
を参照外しすると、 Foo
が返されるということになります。これはかなり奇怪なことです。参照外しは通常 T
の参照に対して T
を返します。例で扱っているのは2つの関連しない型です。しかしながら、ドット演算子は暗黙的な参照外しを行うため、メソッド呼び出しは Bar
に対してと同様に Foo
のメソッドを検索することになります。
長所
ちょっとしたボイラーテンプレートを省けます。例えは、
impl Bar {
fn m(&self) {
self.f.m()
}
}
短所
最も重要なことは、これは人を驚かせるイディオムであるということです - コードのこれを読む後のプログラマーは、このようなことが起こるとは思わないでしょう。これは私たちが Deref
トレイトを、その意図 (およびドキュメント等) に反して、誤用しているからです。またこのメカニズムは完全に暗黙的なものだからです。
このパターンでは、JavaやC++の継承のようにFoo
と Bar
の間に部分型を導入することはありません。さらに、 Foo
で実装された trait は自動的に Bar
で実装されるわけではありません。そのため、このパターンは境界チェックと相性が悪くなり、それゆえにジェネリックプログラミングとの相性も悪くなります。
このパターンを使うことは、self
に関して、ほとんどのOO言語とは微妙に異なるセマンティクスを与えます。通常はこれはサブクラスに対する参照ですが、このパターンではメソッドが定義されている「クラス」に対するものになります。
最後に、このパターンは単一継承のみをサポートし、インターフェイス、クラスベースのプライバシー、その他の継承に関連する機能はありません。そのためJavaの継承などに慣れているプログラマにとっては、微妙に驚くような経験をすることになります。
議論
唯一の良い代替案はありません。それぞれの状況によってはトレイトを再実装するのがよいかもしれませんし、または手動で Foo
にディスパッチするファサードメソッドを書くのがよいかもしれません。このような継承の仕組みがRustに追加されようとしていますが、しかし安定版の Rust に至るまでに時間がかかりそうです。詳細は blog posts や RFC issue を参照してください。
Deref
トレイトはカスタムポインタ型の実装のために設計されています。これは、T
へのポインタを T
に変換するもので、異なる型間の変換を行うものではありません。このことをトレイトの定義により強制されない (おそらくはできない) のは残念なことです。
Rustは、明示的なメカニズムと暗黙的なメカニズムのバランスを慎重に取ろうとしています。そして型間の変換では明示的な変換を支持しています。ドット演算子での自動参照外しは、人間工学的に暗黙的なメカニズムを強く支持するケースですが、これは間接的な程度に限定されており、任意の型間の変換を行うものではありません。
See also
- イディオムの スマートポインタとしてのコレクション。
- delegateやambassadorような、ボイラーテンプレートを減らすデリゲーションcrate。
Deref
トレイトのドキュメント.
Rustので関数型使用法
Rustは命令型言語ですが、数々の関数型プログラミング のパラダイムを模範としています。
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions that each return a value, rather than a sequence of imperative statements which change the state of the program. (コンピュータサイエンスにおいて、 関数型プログラミング とは、関数の適用と合成によってプログラムを構成するプログラミングパラダイムです。これは宣言的なプログラミングパラダイムです。その関数の定義は、プログラムの状態を変更する命令文の列ではなく、値を返す式のツリーです。)
プログラミングパラダイム
関数型プログラミングを理解する上で、命令型プログラミング出身者にとって最大のハードルのひとつは、考え方の転換です。命令型プログラムは どうやるのか を記述するのに対し、宣言型プログラムは 何をするのか を記述します。これを示すために1から10までの数字を合計してみましょう。
命令型
#![allow(unused)] fn main() { let mut sum = 0; for i in 1..11 { sum += i; } println!("{sum}"); }
命令型プログラムでは、何が起こっているのかを確認するためにコンパイラを演じなければなりません。ここでは、まず sum
が 0
から始めます。次に、1から10までの範囲を繰り返します。ループの各回で、ループ範囲内の対応する値を加算します。 そして結果を出力します。
i | sum |
---|---|
1 | 1 |
2 | 3 |
3 | 6 |
4 | 10 |
5 | 15 |
6 | 21 |
7 | 28 |
8 | 36 |
9 | 45 |
10 | 55 |
私たちの多くは、こうしてプログラミングを始めました。私たちはプログラムをステップの集まりだと学んできました。
宣言的
#![allow(unused)] fn main() { println!("{}", (1..11).fold(0, |a, b| a + b)); }
おぉ!これは全く異なりますね!何が起こっているのでしょう?宣言型プログラムでは、どうやるのか ではなく、何をするのか を記述していることに留意してください。fold
は関数を合成する関数です。この関数名はHaskellからの慣例です。
ここでは、足し算の関数(クロージャ:|a, b| a + b
)を1から10までの範囲に対し合成しています。この 0
は開始点です。よって a
は最初は 0
です。b
は範囲の最初の要素である 1
です。 0 + 1 = 1
が結果になります。そして a = 1
、 b=2
としてまた fold
(折り返)し、 1 + 2 = 3
が次の結果になります。このプロセスは範囲の最後の要素である 10
まで繰り返されます。
a | b | 結果 |
---|---|---|
0 | 1 | 1 |
1 | 2 | 3 |
3 | 3 | 6 |
6 | 4 | 10 |
10 | 5 | 15 |
15 | 6 | 21 |
21 | 7 | 28 |
28 | 8 | 36 |
36 | 9 | 45 |
45 | 10 | 55 |
型クラスとしてのジェネリック
説明
Rustの型システムは、(JavaやC++のような)命令型言語ではなく、(Haskellのような)関数型言語のように設計されています。その結果、Rustは多くのプログラミング上の問題を「静的型付け」の問題にすることができます。これは関数型言語を選択する最大の利点の1つでありまたRustのコンパイル時保証の多くにとって重要なものです。
この考え方の重要な部分は、ジェネリック型の動作方法です。例えばC++やJavaでは、ジェネリック型はコンパイラのメタプログラミングの構成要素です。C++ における vector<int>
や vector<char>
は、 vector
型のボイラーテンプレート ( template
として知られるもの) の (それに異なる2つの型を埋め込んだ)異なる2つのコピーにすぎません。
Rustでは、ジェネリック型パラメータは関数型言語で「型クラス制約」として知られているものを作成します。 またエンドユーザが埋め込むそれぞれの異なるパラメータは、 実際に型を変えます 。言い換えると、 Vec<isize> と
Vec
これは モノモーフィズム(単相化) と呼ばれ、 ポリモーフィック(多相の) コードから異なる型が作成されます。この特別な振る舞いには、impl
ブロックにジェネリックパラメータを指定する必要があります。ジェネリック型に異なる値を与えれば、異なる型が生成されます。そして、異なる型は異なる impl
ブロックを持つことができます。
オブジェクト指向言語では、クラスは親から振る舞いを継承することができます。しかしながらこれは、ある型クラスの特定のメンバに対して、追加的な振る舞いだけを付加することを可能にするにとどまらず、余分な振る舞いを付加することも同様に許容してしまいます。
最も近いのは、JavascriptやPythonのランタイムポリモーフィズムです。これらの言語では、あらゆるコンストラクタにより、思ったときにオブジェクトに新しいメンバを追加可能です。しかしながら、これらの言語と異なりRustでは、追加されたメソッドは使用時にすべて型チェックされます。なぜなら、それらジェネリックは静的に定義されているからです。これはより使いやすく、かつ安全性を確保したものになっています。
例
あなたは、一連のラボマシン用のストレージサーバを設計しているとします。関連するソフトウェアのため、次の2つの異なるプロトコルをサポートする必要があります: BOOTP (PXEネットワークブート用) とNFS (リモートマウントストレージ用) です。
あなたの目標は、Rustで書かれた1つのプログラムで、その両方を処理できるようにすることです。このプログラムは (2つの) プロトコルハンドラを持ち、両方のリクエストを待ち受けます。そしてメインのアプリケーションロジックは、ラボの管理者が、実際のファイルのための、ストレージとセキュリティ制御を設定できるようにします。
ラボのマシンからの、ファイルに対してのリクエストは同じ基本情報を含みます。どのプロトコルからであっても、認証方法と取得したいファイルの名前はあります。ストレートな実装はこのようになるでしょう:
enum AuthInfo {
Nfs(crate::nfs::AuthInfo),
Bootp(crate::bootp::AuthInfo),
}
struct FileDownloadRequest {
file_name: PathBuf,
authentication: AuthInfo,
}
この設計は十分に機能するかもしれません。しかし、次に プロトコル固有 のメタデータの追加をサポートする必要があるとします。例えば、NFSでは、追加のセキュリティルールを適用するために、そのマウントポイントが何であるかを確定したいとします。
現在の構造体の設計では、プロトコルは実行時まで決定されません。つまり、一方のプロトコルにて適用され、もう一方では適用されないメソッドは、プログラマが実行時にチェックを行う必要があります。
NFSマウントポイントの取得方法は以下のようになります:
struct FileDownloadRequest {
file_name: PathBuf,
authentication: AuthInfo,
mount_point: Option<PathBuf>,
}
impl FileDownloadRequest {
// ... その他のメソッド ...
/// NFSリクエストであれば、NFSマウントポイントを取得する。
///そうでなければ None を返す。
pub fn mount_point(&self) -> Option<&Path> {
self.mount_point.as_ref()
}
}
mount_point()
のすべての呼び出し元は None
をチェックして、それを処理するコードを書かなければなりません。これは、たとえそのコードパス上ではNFS リクエストしか使われないと知っていたとしてもです!
もし異なるリクエストタイプが混同されるのであれば、コンパイル時エラーを発生させる方がはるかに最適でしょう。結局のところ、ユーザコードのすべてのパスは、このライブラリのどの関数を使うのかも含めて、あるリクエストがNFSリクエストかBOOTPリクエストか、わかっていることでしょう。
Rustでは、実際にこれができます!解決策は、APIを分割するために ジェネリック型を追加 することです。
これは以下のようになります:
use std::path::{Path, PathBuf}; mod nfs { #[derive(Clone)] pub(crate) struct AuthInfo(String); // NFSセッション管理は省略 } mod bootp { pub(crate) struct AuthInfo(); // bootp では認証なし } // 外部のユーザが独自のプロトコルを作れないようにするための、 private なモジュールです! mod proto_trait { use super::{bootp, nfs}; use std::path::{Path, PathBuf}; pub(crate) trait ProtoKind { type AuthInfo; fn auth_info(&self) -> Self::AuthInfo; } pub struct Nfs { auth: nfs::AuthInfo, mount_point: PathBuf, } impl Nfs { pub(crate) fn mount_point(&self) -> &Path { &self.mount_point } } impl ProtoKind for Nfs { type AuthInfo = nfs::AuthInfo; fn auth_info(&self) -> Self::AuthInfo { self.auth.clone() } } pub struct Bootp(); // 追加のメタデータなし impl ProtoKind for Bootp { type AuthInfo = bootp::AuthInfo; fn auth_info(&self) -> Self::AuthInfo { bootp::AuthInfo() } } } use proto_trait::ProtoKind; // impl を防ぐため、非publicを維持 pub use proto_trait::{Bootp, Nfs}; // 呼び出し元に見えるよう再エクスポート struct FileDownloadRequest<P: ProtoKind> { file_name: PathBuf, protocol: P, } // すべての共通のAPIは、ジェネリックの impl ブロックに入れます impl<P: ProtoKind> FileDownloadRequest<P> { fn file_path(&self) -> &Path { &self.file_name } fn auth_info(&self) -> P::AuthInfo { self.protocol.auth_info() } } // すべてのプロトコル固有の実装は、それぞれのブロックに入れます impl FileDownloadRequest<Nfs> { fn mount_point(&self) -> &Path { self.protocol.mount_point() } } fn main() { // ここにあなたのコード }
このアプローチにて、もしもユーザが間違えて異なる型を使ってしまったとき;
fn main() {
let mut socket = crate::bootp::listen()?;
while let Some(request) = socket.next_request()? {
match request.mount_point().as_ref() {
"/secure" => socket.send("Access denied"),
_ => {} // 続く...
}
// ここに残りのコード
}
}
構文エラーが発生します。型 FileDownloadRequest<Bootp>
は mount_point()
を実装していません。FileDownloadRequest<Nfs>
型だけが実装しています。またもちろん、これはNFSモジュールによって作成されるもので、BOOTPモジュールによるって作成されるものではありません!
長所
第一に、複数のステートに共通するフィールドの重複をなくすことができます。非共有のフィールドをジェネリックなものにすることで、それらは一度だけ実装されます。
第二に、状態別に分割されているため、impl
ブロックが読みやすくなります。すべてのステートに共通するメソッドは1つのブロックに1回だけ記述され、あるステートに固有のメソッドは別のブロックに記述されます。
どちらも、コード行数がより少なく、よく整理されているということになります。
短所
現時点では、コンパイラの単相化の実装方法により、バイナリのサイズが大きくなります。将来的に実装が改善されることを願っています。
代替案
-
型が、その構築や部分的な初期化のために「分割API」を必要とするようであれば、Builderパターンを検討してください。
-
型間のAPIが変化しない場合 – 振る舞いだけが変化する場合 – は代わりにStrategyパターンを使うのがよいでしょう。
See also
このパターンは標準ライブラリ全体で使われています:
Vec<u8>
は、他の全てのVec<T>
の型とは異なり、Stringからキャストすることができます。1- また、バイナリヒープにキャストすることもできます。ただしそれが
Ord
トレイトを実装している場合に限ります。2 to_string
メソッドは、str
型に対してのCow
について特殊化されています。3
また、APIの柔軟性のために、いくつかの人気のあるクレートで使用されています:
-
組み込み機器のための
embedded-hal
エコシステムは、このパターンを多用しています。例えば、組み込みのピンの制御に使われるデバイスレジスタの設定を静的に検証することができます。ピンがあるモードになると、Pin<MODE>
構造体を返します。そのジェネリックはそのモードで使用可能な機能を決定します。 それら機能はPin
自体には実装されません。 4 -
HTTPクライアントライブラリ
hyper
は、このパターンを使い、さまざまなプラグイン可能なリクエストに対して、リッチなAPIを公開しています。異なるコネクタを持つクライアントは、異なるメソッドと異なるトレイト実装を持ちますが、メソッドのコアセットはすべてのコネクタに適用されます。5 -
「Type State」パターン – 内部状態や不変量に基づいてオブジェクトがAPIを得たり失ったりするパターン – は、Rustで同じ基本概念と少し異なる手法を使って実装されています。6
関数型言語のオプティクス
Opticsは、関数型言語の一般的なAPI設計の一種です。これは純粋関数風のコンセプトであり、Rustではあまり使われません。
とはいえ、このコンセプトを探求することは、Rust APIにおける他のパターン、例えばvisitors 、を理解するのに役立つかもしれません。これらもニッチなユースケースがあります。
これは非常に大きなトピックであり、その能力を完全に理解するには、言語設計に関する実際の書籍が必要になるでしょう。しかし、Rustでの適用はもっと簡単です。
コンセプトの関連部分を説明するために、Serde
-APIを例として使用します。これは、単にAPIドキュメントから理解することが、多くの人によって難しいものの一つだからです。
その過程で、オプティクスと呼ばれる、ある別のパターンをカバーします。 Iso 、 Poly Iso、 Prism です。
APIの例: Serde
APIを読むだけで、Serde の仕組みを理解しようとすることは難しく、初めてなら尚の事です。Deserializer
トレイトについて考えてみましょう。新しいデータフォーマットをパースするライブラリによって実装されるものです:
pub trait Deserializer<'de>: Sized {
type Error: Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>;
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>;
// あとは省略
}
そして以下が、ジェネリックに渡される Visitor
トレイトの定義です:
pub trait Visitor<'de>: Sized {
type Value;
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: Error;
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error;
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error;
// あとは省略
}
ここでは多くの型消去が行われており、複数のレベルでの関連型が行ったり来たりしています。
これを理解するためには、lensの概念を念頭に置き、総称的に渡される Visitor
型の定義を見てみる必要があります:
これを理解するひとつの方法が、関数型言語の概念である オプティクス(Optics) を考察することです。
これは、振る舞いとプロパティの合成を行うための方法であり、Rustにて共通するパターン (エラー処理、型変換など) を容易にするように設計されたものです。
これらの値は、デシリアライズされる Rust の値の一部を表します。 失敗した場合は Error
型を返します。 Deserializer
によって、メソッドが呼び出されたときに決定されたエラー型です。
これはおそらく、それらAPIが達成しようとするもの、つまりは合成可能性の特異性、を明るみに出すことでしょう。
基本オプティクス
Iso
Iso は、2つの型間の値変換器です。これは非常にシンプルなものですが、概念的に重要な構成要素です。
例として、文書のコンコルダンス (用語索引) として使用されるカスタム・ハッシュ・テーブル構造があるとします1。これはキー (単語) には文字列を使用し、値 (例えばファイルオフセット) にはインデックスのリストを使用します。
重要な機能は、このフォーマットをディスクにシリアライズできることです。「クイック&ダーティ」なアプローチは、JSON形式の文字列との相互の変換を実装することでしょう。(当面、エラーは無視し、後に扱います)
関数型言語のユーザーが期待する正規系で記述すると:
case class ConcordanceSerDe {
serialize: Concordance -> String
deserialize: String -> Concordance
}
Isoはこのように、異なる型の値を変換する関数のペア: serialize
と deserialize
です。
単純な実装:
#![allow(unused)] fn main() { use std::collections::HashMap; struct Concordance { keys: HashMap<String, usize>, value_table: Vec<(usize, usize)>, } struct ConcordanceSerde {} impl ConcordanceSerde { fn serialize(value: Concordance) -> String { todo!() } // 無効なコンコルダンスは空 fn deserialize(value: String) -> Concordance { todo!() } } }
これは馬鹿げたものに見えるかもしれません。Rustでは、この種の振る舞いは一般的にトレイトで行います。なにせ、標準ライブラリには FromStr
と ToString
があります。
しかし、そこで次のテーマ: Poly Iso が登場します。
Poly Isos
前の例では、単純に2つの固定型の値を相互に変換するだけでした。この次のブロックは、その上にジェネリクスを加えるもので、より興味深いものです。
Poly Iso は、ある操作が、ある単一の型を返しつつ、任意の型に対しジェネリックなものとなれるようにします。
これでパースに近づけました。エラーケースを無視して、基本的なパーサーが何をするか考えてみよう。繰り返しますが、これが正規形です:
case class Serde[T] {
deserialize(String) -> T
serialize(T) -> String
}
ここで私たちは、最初のジェネリック型、変換される T
を手にしました。
Rustでは、これは標準ライブラリの2つのトレイト: FromStr
とToString
の組にて実装可能です。Rustのバージョンはエラーも処理します:
pub trait FromStr: Sized {
type Err;
fn from_str(s: &str) -> Result<Self, Self::Err>;
}
pub trait ToString {
fn to_string(&self) -> String;
}
Isoとは異なり、Poly Isoは複数の型に適用でき、それらを汎用的に返します。これは、基本的な文字列パーサーに求められるものです。
一見したところ、これはパーサーを記述するための良い選択肢であるように思えます。実際にやってみましょう:
use anyhow;
use std::str::FromStr;
struct TestStruct {
a: usize,
b: String,
}
impl FromStr for TestStruct {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<TestStruct, Self::Err> {
todo!()
}
}
impl ToString for TestStruct {
fn to_string(&self) -> String {
todo!()
}
}
fn main() {
let a = TestStruct {
a: 5,
b: "hello".to_string(),
};
println!("Our Test Struct as JSON: {}", a.to_string());
}
極めて論理的なものに見えます。しかしながら、ここには2つの問題があります。
第一に、to_string
はAPIのユーザーに「これはJSONです」ということを示しません。すべての型がJSON表現に対応している必要がありますが、Rust標準ライブラリの型の多くがすでに対応するものでありません。これを使用するのはあまり適格でないでしょう。これは、独自のトレイトで簡単に解決できます。
ですが、第二の微妙な問題があります。スケーリングです。
すべての型に手作業で to_string
を書くのであれば、これは機能します。しかし、自分の型をシリアライズ可能にしたいすべての人が、たくさんのコード – そしておそらくは別のJSONライブラリ – を自分で書かなければならないのであれば、これはあっという間にめちゃくちゃなことになってしまうでしょう!
これの解答は、Serdeの2つの重要なイノベーションの1つです。データシリアライズ言語間に共通する構造としてRustデータを表現する、独立したデータモデルです。これにより、Rustのコード生成能力を活かして、 Visitor
と呼ばれる、中間的な変換型を生成することが可能となります。
つまり、正規形では (これも簡潔のためにエラー処理は省略):
case class Serde[T] {
deserialize: Visitor[T] -> T
serialize: T -> Visitor[T]
}
case class Visitor[T] {
toJson: Visitor[T] -> String
fromJson: String -> Visitor[T]
}
その結果は、Poly Iso と Iso が (それぞれ) 1つになります。この2つは、どちらもトレイトで実装可能です:
#![allow(unused)] fn main() { trait Serde { type V; fn deserialize(visitor: Self::V) -> Self; fn serialize(self) -> Self::V; } trait Visitor { fn to_json(self) -> String; fn from_json(json: String) -> Self; } }
Rustの構造体を独立した形式に変換するための統一されたルールがあるために、型T
に関連する Visitor
を作成するコード生成も可能です:
#[derive(Default, Serde)] // "Serde" の derive は trait impl ブロックを生成します
struct TestStruct {
a: usize,
b: String,
}
// ユーザは、このマクロを記述して、関連する Visitor 型を生成します
generate_visitor!(TestStruct);
では、次のようにするのでしょうか?
fn main() {
let a = TestStruct { a: 5, b: "hello".to_string() };
let a_data = a.serialize().to_json();
println!("Our Test Struct as JSON: {a_data}");
let b = TestStruct::deserialize(
generated_visitor_for!(TestStruct)::from_json(a_data));
}
これは変換が、結局のところ対称的でないことを明らかにしました!紙上ではともかく、自動生成コードを使うと、String
からの変換に必要となる実際の型の名前が隠されています。型名を得るために、 generated_visitor_for!
マクロのようなものが必要になります。
この方法は歪ですが、しかし機能しています…私たちが、分かっていても触れたくないものに手を付けるまでは。
現在サポートしているフォーマットはJSONだけです。より多くのフォーマットをサポートするにはどうすればよいでしょうか?
現在の設計では、すべてのコード生成を完全に書き直し、新しい Serde トレイトを作成する必要があります。これは非常に恐ろしいことで、拡張性がまったくありません!
これを解決するには、もっと強力なものが必要です。
Prism
フォーマットを考慮に入れるには、次のような正規形が必要です:
case class Serde[T, F] {
serialize: T, F -> String
deserialize: String, F -> Result[T, Error]
}
この構成は Prism と呼ばれます。Poly Isoより「一段上」ジェネリックなものです (この場合、「交差する」型Fがキーです) 。
残念ながら、Visitor
はトレイトなので (各具象が独自のカスタムコードを必要とするため) 、Rustがサポートしていないジェネリックの型境界のようなものが必要になります。
幸いなことに、私たちには Visitor
タイプがまだあります。 Visitor
は何をするものでしょうか?各データ構造がそれ自身をパースする方法を定義可能にしようとするものです。
では、ジェネリックなフォーマット用のインターフェースをもう一つ追加できるとしたらどうでしょう?そうすれば、Visitor
は単なる実装のディテールに過ぎず、2つのAPIの「橋渡し」になるでしょう。
正規形では:
case class Serde[T] {
serialize: F -> String
deserialize F, String -> Result[T, Error]
}
case class VisitorForT {
build: F, String -> Result[T, Error]
decompose: F, T -> String
}
case class SerdeFormat[T, V] {
toString: T, V -> String
fromString: V, String -> Result[T, Error]
}
なんと、下部にあるのは、トレイトとして実装可能なPoly Isoのペアです!
こうして私たちは Serde API を得ました:
- シリアライズされる各型は
Serde
クラスに対応するDeserialize
かSerialize
を実装します - それらはVisitorトレイトを実装したある型 (各方向に1つずつの2つ) を持ちます。これは通常 (常にではありませんが) deriveマクロによって生成されるコードによって行われます。これには、データの型とSerdeデータモデルのフォーマットとの間でコンストラクトやデストラクトを行うロジックが含まれています。
Deserializer
を実装した型は、Visitor
によって「駆動」されて、フォーマットに固有のすべてのディテールを処理します。
この分割とRust型消去は、まさにPrismを間接的に実現するためのものです。
これは Deserializer
トレイトで確認することが可能です
pub trait Deserializer<'de>: Sized {
type Error: Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>;
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>;
// あとは省略
}
そしてVisitorは:
pub trait Visitor<'de>: Sized {
type Value;
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: Error;
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error;
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error;
// あとは省略
}
そして、マクロによって実装された Deserialize
トレイト:
pub trait Deserialize<'de>: Sized {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
}
これは抽象的なものでしたから、具体的な例を見てみましょう。
Serde はどのようにJSONの断片を前述の struct Concordance
にデシリアライズするでしょうか?
- ユーザーは、データをデシリアライズするため、ライブラリ関数を呼び出します。これは JSONフォーマットに基づく
Deserializer
を生成します。 - 構造体のフィールドに基づき、それを表現するために必要とされる汎用データモデルの各型 (
Vec
(リスト)、u64
、String
) の生成方法を知っているVisitor
が生成 (詳細は後述) されます。 - デシリアライザーは、アイテムをパースするに際し、
Visitor
を呼び出します。 Visitor
は、見つかったアイテムが期待されたものであるかどうかを示し、もしそうでなければ、デシリアライズに失敗したことを示すエラーを示します。
上記の非常に単純な構造体の場合、期待されるパターンは次のようになります:
- マップ ( _Serde_の
HashMap
やJSONのディクショナリに相当するもの) への訪問を開始。 - 「keys」という文字列キーを訪問。
- マップの値の訪問を開始。
- 各アイテムについて、文字列キーと続いて整数値を訪問。
- マップの終端を訪問。
- マップをデータ構造の
keys
フィールドに格納。 - 「value_table」という文字列キーを訪問。
- リスト値の訪問を開始。
- 各アイテムについて、整数を訪問。
- リストの終端を訪問
- リストを
value_table
フィールドに格納。 - マップの終端を訪問。
org/stable/src/alloc/string.rs.html#2235-2240)
例: https://docs.rs/stm32f30x-hal/0.1.0/stm32f30x_hal/gpio/gpioa/struct.PA0.html
Serde solves this usability challenge with a derive macro:
use serde::Deserialize;
#[derive(Deserialize)]
struct IdRecord {
name: String,
customer_id: String,
}
このマクロは単に、構造体に Deserialize
と呼ばれるトレイトを実装させる impl ブロックを生成します。
これは、構造体自体の作成方法を決定する関数です。コードは構造体のフィールドに基づいて生成されます。構文解析ライブラリ (私たちの例ではJSON構文解析ライブラリ) が呼び出されると、Deserializer
が作成され、それをパラメータとして Type::deserialize
が呼び出されます。
これは非常に大きなトピックであり、その能力を完全に理解するには、言語設計に関する実際の書籍が必要になるでしょう。しかし、Rustでの適用はもっと簡単です。
完全な例については、Serde documentation を参照してください。
その結果、デシリアライズされる型はAPIの 「トップレイヤー」 のみを実装し、ファイルフォーマットは 「ボトムレイヤー」 のみを実装すればよいことになります。ジェネリック型がそれらの橋渡しをするために、各ピースは残りのエコシステムと「単純に動作する」ことができます。
その過程で、オプティクスと呼ばれる、ある別のパターンをカバーします。 Iso 、 Poly Iso、 Prism です。
このトピックについてもっと知りたい方は、以下のセクションをご覧ください。
あわせて読みたい
- lens-rs クレートは、これらの例よりもすっきりとしたインターフェイスを持つ、プリビルドなlensの実装です
- serde 自体が、これらのコンセプトを、エンドユーザが詳細を知ることなく、直感的に理解可能なもの (すなわち構造体の定義) にするものです
- luminanceは、レンズAPIの設計を使用した、コンピュータグラフィックスを描画するためのクレートです。手続き型マクロを持ち、このマクロは、異なるピクセルタイプのバッファに対する完全なprismを汎用性を保ち作成します
- Scala における lens についての記事 は、Scalaの経験なしでもとても読みやすいものです。
- 論文:Profunctor Optics: Modular Data Accessors
- Musliは、似たような構造を異なるアプローチ、たとえば visitor を使わずに、で使おうとするライブラリである
(https://web.archive.org/web/20221128190041/https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial)
追加リソース
A collection of complementary helpful content
トーク
- Design Patterns in Rust by Nicholas Cameron at the PDRust (2016)
- Writing Idiomatic Libraries in Rust by Pascal Hertleif at RustFest (2017)
- Rust Programming Techniques by Nicholas Cameron at LinuxConfAu (2018)
書籍 (オンライン)
設計原則
一般的な設計原則の概要
SOLID
- 単一責任の原則(SRP): クラスは単一の責任だけを持つべきです。 すなわち、ソフトウェアの仕様の一部分の変更のみが、クラスの仕様に影響を与えることができるべきです。
-
- 開放/閉鎖原則(OCP): “Software entities … should be open for extension, but closed for modification.(ソフトウェアエンティティは … 拡張に対して開放され、変更に対して閉鎖されているべきです)”
- リスコフの置換原則(LSP): “Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.(プログラム中のオブジェクトは、プログラムを修正することなく、 そのサブタイプのインスタンスと置き換え可能であるべきです。”
- インタフェース分離の原則(ISP): “Many client-specific interfaces are better than one general-purpose interface.(多くのクライアント固有のインタフェースは、1つの汎用インタフェースよりも優れている)”
- 依存関係逆転の原則(DIP): “depend upon abstractions, [not] concretions.(抽象的なものに依存すべきであり、具体的なものに依存すべきではない)”。
DRY (Don’t Repeat Yourself)
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system(すべての知識は、システム内で単一の、曖昧さのない、権威ある表現を持たなければなりません)”
KISSの原則
これでパースに近づけました。エラーケースを無視して、基本的なパーサーが何をするか考えてみよう。繰り返しますが、これが正規形です:
デメテルの法則(LoD)
Rustでは、これは標準ライブラリの2つのトレイト: FromStr
とToString
の組にて実装可能です。Rustのバージョンはエラーも処理します:
契約による設計(DbC)
一見したところ、これはパーサーを記述するための良い選択肢であるように思えます。実際にやってみましょう:
カプセル化
第一に、to_string
はAPIのユーザーに「これはJSONです」ということを示しません。すべての型がJSON表現に対応している必要がありますが、Rust標準ライブラリの型の多くがすでに対応するものでありません。これを使用するのはあまり適格でないでしょう。これは、独自のトレイトで簡単に解決できます。
コマンド-クエリの分離(CQS)
“Functions should not produce abstract side effects…only commands (procedures) will be permitted to produce side effects. (関数は副作用を発生されてはいけません … コマンド(プロシージャ)だけが副作用を発生することを許容されます)” - Bertrand Meyer: Object-Oriented Software Construction
驚き最小の原則(POLA)
システムの構成要素はそのユーザが期待するように振舞わねばならず、その振る舞いはユーザを驚かせるものであってはなりません
言語モジュール単位(Linguistic-Modular-Units)
“Modules must correspond to syntactic units in the language used. (モジュールは使用言語の構文単位に対応しなければならない)” - Bertrand Meyer: Object-Oriented Software Construction
自己文書化(Self-Documentation)
現在サポートしているフォーマットはJSONだけです。より多くのフォーマットをサポートするにはどうすればよいでしょうか?
統一的アクセス(Uniform-Access)
“All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation. (モジュールにより提供されるすべてのサービスは、ストレージによって実装されるのか、計算により実装されるのかを暴露しない、統一された表記法により利用可能であるべきです。)” - Bertrand Meyer: Object-Oriented Software Construction
単一選択(Single-Choice)
“Whenever a software system must support a set of alternatives, one and only one module in the system should know their exhaustive list. (ソフトウェアシステムが複数の選択肢をサポートしなければならないときは常に、その網羅的なリストを知っているのはシステム内の単一のモジュールのみであるべきです)” - Bertrand Meyer: Object-Oriented Software Construction
永続的クロージャ(Persistence-Closure)
“Whenever a storage mechanism stores an object, it must store with it the dependents of that object. Whenever a retrieval mechanism retrieves a previously stored object, it must also retrieve any dependent of that object that has not yet been retrieved. (格納メカニズムがオブジェクトを格納するときは常に、 そのオブジェクトに従属するものを一緒に格納しなければなりません。また、取出メカニズムが以前に格納されたオブジェクトを取り出すときは常に、オブジェクトに従属する、また取り出されていないものを取り出さねばなりません。)” - Bertrand Meyer: Object-Oriented Software Construction