オブジェクト指向デザインパターンを実装する
ステートパターンは、オブジェクト指向デザインパターンの1つです。このパターンの肝は、 値が一連のステートオブジェクトで表されるなんらかの内部状態を持ち、 その内部の状態に基づいて値の振る舞いが変化するというものです。ステートオブジェクトは、 機能を共有します: Rustでは、もちろん、オブジェクトと継承ではなく、構造体とトレイトを使用します。 各ステートオブジェクトは、自身の振る舞いと別の状態に変化すべき時を司ることに責任を持ちます。 ステートオブジェクトを保持する値は、状態ごとの異なる振る舞いや、いつ状態が移行するかについては何も知りません。
ステートパターンを使用することは、プログラムの業務要件が変わる時、状態を保持する値のコードや、 値を使用するコードを変更する必要はないことを意味します。ステートオブジェクトの1つのコードを更新して、 規則を変更したり、あるいはおそらくステートオブジェクトを追加する必要しかないのです。 ステートデザインパターンの例と、そのRustでの使用方法を見ましょう。
ブログ記事のワークフローを少しずつ実装していきます。ブログの最終的な機能は以下のような感じになるでしょう:
- ブログ記事は、空の草稿から始まる。
- 草稿ができたら、査読が要求される。
- 記事が承認されたら、公開される。
- 公開されたブログ記事だけが表示する内容を返すので、未承認の記事は、誤って公開されない。
それ以外の記事に対する変更は、効果を持つべきではありません。例えば、査読を要求する前にブログ記事の草稿を承認しようとしたら、 記事は、非公開の草稿のままになるべきです。
リスト17-11は、このワークフローをコードの形で示しています: これは、
blog
というライブラリクレートに実装するAPIの使用例です。まだblog
クレートを実装していないので、
コンパイルはできません。
ファイル名: src/main.rs
extern crate blog;
use blog::Post;
fn main() {
let mut post = Post::new();
// 今日はお昼にサラダを食べた
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
ユーザがPost::new
で新しいブログ記事の草稿を作成できるようにしたいです。それから、
草稿状態の間にブログ記事にテキストを追加できるようにしたいです。承認前に記事の内容を即座に得ようとしたら、
記事はまだ草稿なので、何も起きるべきではありません。デモ目的でコードにassert_eq!
を追加しました。
これに対する素晴らしい単体テストは、ブログ記事の草稿がcontent
メソッドから空の文字列を返すことをアサートすることでしょうが、
この例に対してテストを書くつもりはありません。
次に、記事の査読を要求できるようにしたく、また査読を待機している間はcontent
に空の文字列を返してほしいです。
記事が承認を受けたら、公開されるべきです。つまり、content
を呼んだ時に記事のテキストが返されるということです。
クレートから相互作用している唯一の型は、Post
だけであることに注意してください。
この型はステートパターンを使用し、記事がなり得る種々の状態を表す3つのステートオブジェクトのうちの1つになる値を保持します。
草稿、査読待ち、公開中です。1つの状態から別の状態への変更は、Post
型内部で管理されます。
Post
インスタンスのライブラリ使用者が呼び出すメソッドに呼応して状態は変化しますが、
状態の変化を直接管理する必要はありません。また、ユーザは、
査読前に記事を公開するなど状態を誤ることはありません。
Post
を定義し、草稿状態で新しいインスタンスを生成する
ライブラリの実装に取り掛かりましょう!なんらかの内容を保持する公開のPost
構造体が必要なことはわかるので、
構造体の定義と、関連する公開のPost
インスタンスを生成するnew
関数から始めましょう。リスト17-12のようにですね。
また、非公開のState
トレイトも作成します。それから、Post
はstate
という非公開のフィールドに、
Option
でBox<State>
のトレイトオブジェクトを保持します。Option
が必要な理由はすぐわかります。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<State>>, content: String, } impl Post { pub fn new() -> Post { Post { state: Some(Box::new(Draft {})), content: String::new(), } } } trait State {} struct Draft {} impl State for Draft {} }
State
トレイトは、異なる記事の状態で共有される振る舞いを定義し、Draft
、PendingReview
、Published
状態は全て、
State
トレイトを実装します。今は、トレイトにメソッドは何もなく、Draft
が記事の初期状態にしたい状態なので、
その状態だけを定義することから始めます。
新しいPost
を作る時、state
フィールドは、Box
を保持するSome
値にセットします。
このBox
がDraft
構造体の新しいインスタンスを指します。これにより、
新しいPost
を作る度に、草稿から始まることが保証されます。Post
のstate
フィールドは非公開なので、
Post
を他の状態で作成する方法はないのです!Post::new
関数では、content
フィールドを新しい空のString
にセットしています。
記事の内容のテキストを格納する
リスト17-11は、add_text
というメソッドを呼び出し、ブログ記事のテキスト内容に追加される&str
を渡せるようになりたいことを示しました。
これをcontent
フィールドをpub
にして晒すのではなく、メソッドとして実装しています。
これは、後ほどcontent
フィールドデータの読まれ方を制御するメソッドを実装できることを意味しています。
add_text
メソッドは非常に素直なので、リスト17-13の実装をimpl Post
ブロックに追加しましょう:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } impl Post { // --snip-- pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } } }
add_text
メソッドは、self
への可変参照を取ります。というのも、add_text
を呼び出したPost
インスタンスを変更しているからです。
それからcontent
のString
に対してpush_str
を呼び出し、text
引数を渡して保存されたcontent
に追加しています。
この振る舞いは、記事の状態によらないので、ステートパターンの一部ではありません。add_text
メソッドは、
state
フィールドと全く相互作用しませんが、サポートしたい振る舞いの一部ではあります。
草稿の記事の内容は空であることを保証する
add_text
を呼び出して記事に内容を追加した後でさえ、記事はまだ草稿状態なので、
それでもcontent
メソッドには空の文字列スライスを返してほしいです。
リスト17-11の8行目で示したようにですね。とりあえず、この要求を実現する最も単純な方法でcontent
メソッドを実装しましょう:
常に空の文字列スライスを返すことです。一旦、記事の状態を変更する能力を実装したら、公開できるように、
これを後ほど変更します。ここまで、記事は草稿状態にしかなり得ないので、記事の内容は常に空のはずです。
リスト17-14は、この仮の実装を表示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } impl Post { // --snip-- pub fn content(&self) -> &str { "" } } }
この追加されたcontent
メソッドとともに、リスト17-11の8行目までのコードは、想定通り動きます。
記事の査読を要求すると、状態が変化する
次に、記事の査読を要求する機能を追加する必要があり、これをすると、状態がDraft
からPendingReview
に変わるはずです。
リスト17-15はこのコードを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- pub fn request_review(&mut self) { if let Some(s) = self.state.take() { self.state = Some(s.request_review()) } } } trait State { fn request_review(self: Box<Self>) -> Box<State>; } struct Draft {} impl State for Draft { fn request_review(self: Box<Self>) -> Box<State> { Box::new(PendingReview {}) } } struct PendingReview {} impl State for PendingReview { fn request_review(self: Box<Self>) -> Box<State> { self } } }
Post
にself
への可変参照を取るrequest_review
という公開メソッドを与えます。それから、
Post
の現在の状態に対して内部のrequest_review
メソッドを呼び出し、
この2番目のrequest_review
が現在の状態を消費し、新しい状態を返します。
State
トレイトにrequest_review
メソッドを追加しました; このトレイトを実装する型は全て、
これでrequest_review
メソッドを実装する必要があります。メソッドの第1引数にself
、&self
、&mut self
ではなく、
self: Box<Self>
としていることに注意してください。この記法は、型を保持するBox
に対して呼ばれた時のみ、
このメソッドが合法になることを意味しています。この記法は、Box<Self>
の所有権を奪い、古い状態を無効化するので、
Post
の状態値は、新しい状態に変形できます。
古い状態を消費するために、request_review
メソッドは、状態値の所有権を奪う必要があります。
ここでPost
のstate
フィールドのOption
が問題になるのです: take
メソッドを呼び出して、
state
フィールドからSome
値を取り出し、その箇所にNone
を残します。なぜなら、Rustは、
構造体に未代入のフィールドを持たせてくれないからです。これにより、借用するのではなく、
Post
のstate
値をムーブすることができます。それから、記事のstate
値をこの処理の結果にセットするのです。
self.state = self.state.request_review();
のようなコードで直接state
値の所有権を得るよう設定するのではなく、
一時的にNone
にstate
をセットする必要があります。これにより、新しい状態に変形した後に、
Post
が古いstate
値を使えないことが保証されるのです。
Draft
のrequest_review
メソッドは、新しいPendingReview
構造体の新しいボックスのインスタンスを返す必要があり、
これが、記事が査読待ちの時の状態を表します。PendingReview
構造体もrequest_review
メソッドを実装しますが、
何も変形はしません。むしろ、自身を返します。というのも、既にPendingReview
状態にある記事の査読を要求したら、
PendingReview
状態に留まるべきだからです。
ようやくステートパターンの利点が見えてき始めました: state
値が何であれ、Post
のrequest_review
メソッドは同じです。
各状態は、独自の規則にのみ責任を持ちます。
Post
のcontent
メソッドを空の文字列スライスを返してそのままにします。
これでPost
はPendingReview
とDraft
状態になり得ますが、PendingReview
状態でも、
同じ振る舞いが欲しいです。もうリスト17-11は11行目まで動くようになりました!
content
の振る舞いを変化させるapprove
メソッドを追加する
approve
メソッドは、request_review
メソッドと類似するでしょう: 状態が承認された時に、
現在の状態があるべきと言う値にstate
をセットします。リスト17-16のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- pub fn approve(&mut self) { if let Some(s) = self.state.take() { self.state = Some(s.approve()) } } } trait State { fn request_review(self: Box<Self>) -> Box<State>; fn approve(self: Box<Self>) -> Box<State>; } struct Draft {} impl State for Draft { fn request_review(self: Box<Self>) -> Box<State> { Box::new(PendingReview {}) } // --snip-- fn approve(self: Box<Self>) -> Box<State> { self } } struct PendingReview {} impl State for PendingReview { fn request_review(self: Box<Self>) -> Box<State> { self } // --snip-- fn approve(self: Box<Self>) -> Box<State> { Box::new(Published {}) } } struct Published {} impl State for Published { fn request_review(self: Box<Self>) -> Box<State> { self } fn approve(self: Box<Self>) -> Box<State> { self } } }
State
トレイトにapprove
メソッドを追加し、Published
状態というState
を実装する新しい構造体を追加します。
request_review
のように、Draft
に対してapprove
メソッドを呼び出したら、self
を返すので、
何も効果はありません。PendingReview
に対してapprove
を呼び出すと、
Published
構造体の新しいボックス化されたインスタンスを返します。Published
構造体はState
トレイトを実装し、
request_review
メソッドとapprove
メソッド両方に対して、自身を返します。
そのような場合に記事は、Published
状態に留まるべきだからです。
さて、Post
のcontent
メソッドを更新する必要が出てきました: 状態がPublished
なら、
記事のcontent
フィールドの値を返したいのです; それ以外なら、空の文字列スライスを返したいです。
リスト17-17のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { trait State { fn content<'a>(&self, post: &'a Post) -> &'a str; } pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(&self) } // --snip-- } }
目的は、これらの規則全てをState
を実装する構造体の内部に押し留めることなので、state
の値に対してcontent
メソッドを呼び出し、
記事のインスタンス(要するに、self
)を引数として渡します。そして、state
値のcontent
メソッドを使用したことから返ってきた値を返します。
Option
に対してas_ref
メソッドを呼び出します。値の所有権ではなく、Option
内部の値への参照が欲しいからです。
state
はOption<Box<State>>
なので、as_ref
を呼び出すと、Option<&Box<State>>
が返ってきます。
as_ref
を呼ばなければ、state
を関数引数の借用した&self
からムーブできないので、エラーになるでしょう。
さらにunwrap
メソッドを呼び出し、これは絶対にパニックしないことがわかっています。何故なら、
Post
のメソッドが、それらのメソッドが完了した際にstate
は常にSome
値を含んでいることを保証するからです。
これは、コンパイラには理解不能であるものの、
None
値が絶対にあり得ないとわかる第9章の「コンパイラよりも情報を握っている場合」節で語った一例です。
この時点で、&Box<State>
に対してcontent
を呼び出すと、参照外し型強制が&
とBox
に働くので、
究極的にcontent
メソッドがState
トレイトを実装する型に対して呼び出されることになります。
つまり、content
をState
トレイト定義に追加する必要があり、そこが現在の状態に応じてどの内容を返すべきかというロジックを配置する場所です。
リスト17-18のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String } trait State { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { "" } } // --snip-- struct Published {} impl State for Published { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content } } }
空の文字列スライスを返すデフォルト実装をcontent
メソッドに追加しています。これにより、
Draft
とPendingReview
構造体にcontent
を実装する必要はありません。Published
構造体は、
content
メソッドをオーバーライドし、post.content
の値を返します。
第10章で議論したように、このメソッドにはライフタイム注釈が必要なことに注意してください。
post
への参照を引数として取り、そのpost
の一部への参照を返しているので、
返却される参照のライフタイムは、post
引数のライフタイムに関連します。
出来上がりました。要するに、リスト17-11はもう動くようになったのです!ブログ記事ワークフローの規則でステートパターンを実装しました。
その規則に関連するロジックは、Post
中に散乱するのではなく、ステートオブジェクトに息づいています。
ステートパターンの代償
オブジェクト指向のステートパターンを実装して各状態の時に記事がなり得る異なる種類の振る舞いをカプセル化する能力が、
Rustにあることを示してきました。Post
のメソッドは、種々の振る舞いについては何も知りません。
コードを体系化する仕方によれば、公開された記事が振る舞うことのある様々な方法を知るには、1箇所のみを調べればいいのです:
Published
構造体のState
トレイトの実装です。
ステートパターンを使用しない対立的な実装を作ることになったら、代わりにPost
のメソッドか、
あるいは記事の状態を確認し、それらの箇所(編注
: Post
のメソッドのことか)の振る舞いを変更するmain
コードでさえ、
match
式を使用したかもしれません。そうなると、複数個所を調べて記事が公開状態にあることの裏の意味全てを理解しなければならなくなります!
これは、追加した状態が増えれば、さらに上がるだけでしょう: 各match
式には、別のアームが必要になるのです。
ステートパターンでは、Post
のメソッドとPost
を使用する箇所で、match
式が必要になることはなく、
新しい状態を追加するのにも、新しい構造体を追加し、その1つの構造体にトレイトメソッドを実装するだけでいいわけです。
ステートパターンを使用した実装は、拡張して機能を増やすことが容易です。 ステートパターンを使用するコードの管理の単純さを確認するために、以下の提言を試してみてください:
- 記事の状態を
PendingReview
からDraft
に戻すreject
メソッドを追加する。 - 状態が
Published
に変化させられる前にapprove
を2回呼び出す必要があるようにする。 - 記事が
Draft
状態の時のみテキスト内容をユーザが追加できるようにする。 ヒント: ステートオブジェクトに内容について変わる可能性のあるものの責任を持たせつつも、Post
を変更することには責任を持たせない。
ステートパターンの欠点の1つは、状態が状態間の遷移を実装しているので、状態の一部が密に結合した状態になってしまうことです。
PendingReview
とPublished
の間に、Scheduled
のような別の状態を追加したら、
代わりにPendingReview
のコードをScheduled
に遷移するように変更しなければならないでしょう。
状態が追加されてもPendingReview
を変更する必要がなければ、作業が減りますが、
そうすれば別のデザインパターンに切り替えることになるでしょう。
別の欠点は、ロジックの一部を重複させてしまうことです。重複を除くためには、
State
トレイトのrequest_review
とapprove
メソッドにself
を返すデフォルト実装を試みる可能性があります;
ですが、これはオブジェクト安全性を侵害するでしょう。というのも、具体的なself
が一体なんなのかトレイトには知りようがないからです。
State
をトレイトオブジェクトとして使用できるようにしたいので、メソッドにはオブジェクト安全になってもらう必要があるのです。
他の重複には、Post
のrequest_review
とapprove
メソッドの実装が似ていることが含まれます。
両メソッドはOption
のstate
の値に対する同じメソッドの実装に委譲していて、state
フィールドの新しい値を結果にセットします。
このパターンに従うPost
のメソッドが多くあれば、マクロを定義して繰り返しを排除することも考慮する可能性があります(マクロについては付録Dを参照)。
オブジェクト指向言語で定義されている通り忠実にステートパターンを実装することで、
Rustの強みをできるだけ発揮していません。blog
クレートに対して行える無効な状態と遷移をコンパイルエラーにできる変更に目を向けましょう。
状態と振る舞いを型としてコード化する
ステートパターンを再考して別の代償を得る方法をお見せします。状態と遷移を完全にカプセル化して、 外部のコードに知らせないようにするよりも、状態を異なる型にコード化します。結果的に、 Rustの型検査システムが、公開記事のみが許可される箇所で草稿記事の使用を試みることをコンパイルエラーを発して阻止します。
リスト17-11のmain
の最初の部分を考えましょう:
ファイル名: src/main.rs
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
それでも、Post::new
で草稿状態の新しい記事を生成することと記事の内容にテキストを追加する能力は可能にします。
しかし、空の文字列を返す草稿記事のcontent
メソッドを保持する代わりに、草稿記事は、
content
メソッドを全く持たないようにします。そうすると、草稿記事の内容を得ようとしたら、
メソッドが存在しないというコンパイルエラーになるでしょう。その結果、
誤ってプロダクションコードで草稿記事の内容を表示することが不可能になります。
そのようなコードは、コンパイルさえできないからです。リスト17-19はPost
構造体、DraftPost
構造体、
さらにメソッドの定義を示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } pub struct DraftPost { content: String, } impl Post { pub fn new() -> DraftPost { DraftPost { content: String::new(), } } pub fn content(&self) -> &str { &self.content } } impl DraftPost { pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } } }
Post
とDraftPost
構造体どちらにもブログ記事のテキストを格納する非公開のcontent
フィールドがあります。
状態のコード化を構造体の型に移動したので、この構造体は最早state
フィールドを持ちません。
Post
は公開された記事を表し、content
を返すcontent
メソッドがあります。
それでもPost::new
関数はありますが、Post
のインスタンスを返すのではなく、DraftPost
のインスタンスを返します。
content
は非公開であり、Post
を返す関数も存在しないので、現状Post
のインスタンスを生成することは不可能です。
DraftPost
構造体には、以前のようにテキストをcontent
に追加できるようadd_text
メソッドがありますが、
DraftPost
にはcontent
メソッドが定義されていないことに注目してください!
従って、これでプログラムは、全ての記事が草稿記事から始まり、草稿記事は表示できる内容がないことを保証します。
この制限をかいくぐる試みは、全てコンパイルエラーに落ち着くでしょう。
遷移を異なる型への変形として実装する
では、どうやって公開された記事を得るのでしょうか?公開される前に草稿記事は査読され、
承認されなければならないという規則を強制したいです。査読待ち状態の記事は、それでも内容を表示するべきではありません。
別の構造体PendingReviewPost
を追加し、DraftPost
にPendingReviewPost
を返すrequest_review
メソッドを定義し、
PendingReviewPost
にPost
を返すapprove
メソッドを定義してこれらの制限を実装しましょう。リスト17-20のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } pub struct DraftPost { content: String, } impl DraftPost { // --snip-- pub fn request_review(self) -> PendingReviewPost { PendingReviewPost { content: self.content, } } } pub struct PendingReviewPost { content: String, } impl PendingReviewPost { pub fn approve(self) -> Post { Post { content: self.content, } } } }
request_review
とapprove
メソッドはself
の所有権を奪い、故にDraftPost
とPendingReviewPost
インスタンスを消費し、
それぞれPendingReviewPost
と公開されたPost
に変形します。このように、
DraftPost
インスタンスにrequest_review
を呼んだ後には、DraftPost
インスタンスは生きながらえず、
以下同様です。PendingReviewPost
構造体には、content
メソッドが定義されていないので、
DraftPost
同様に、その内容を読もうとするとコンパイルエラーに落ち着きます。
content
メソッドが確かに定義された公開されたPost
インスタンスを得る唯一の方法が、
PendingReviewPost
に対してapprove
を呼び出すことであり、PendingReviewPost
を得る唯一の方法が、
DraftPost
にrequest_review
を呼び出すことなので、これでブログ記事のワークフローを型システムにコード化しました。
ですが、さらにmain
にも多少小さな変更を行わなければなりません。request_review
とapprove
メソッドは、
呼ばれた構造体を変更するのではなく、新しいインスタンスを返すので、let post =
というシャドーイング代入をもっと追加し、
返却されたインスタンスを保存する必要があります。また、草稿と査読待ち記事の内容を空の文字列でアサートすることも、
する必要もありません: 最早、その状態にある記事の内容を使用しようとするコードはコンパイル不可能だからです。
main
の更新されたコードは、リスト17-21に示されています:
ファイル名: src/main.rs
extern crate blog;
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
post
を再代入するためにmain
に行う必要のあった変更は、この実装がもう、
全くオブジェクト指向のステートパターンに沿っていないことを意味します:
状態間の変形は最早、Post
実装内に完全にカプセル化されていません。
ですが、型システムとコンパイル時に起きる型チェックのおかげでもう無効な状態があり得なくなりました。
これにより、未公開の記事の内容が表示されるなどの特定のバグが、プロダクションコードに移る前に発見されることが保証されます。
blog
クレートに関してこの節の冒頭で触れた追加の要求に提言される作業をそのままリスト17-20の後に試してみて、
このバージョンのコードについてどう思うか確かめてください。この設計では、
既に作業の一部が達成されている可能性があることに注意してください。
Rustは、オブジェクト指向のデザインパターンを実装する能力があるものの、状態を型システムにコード化するなどの他のパターンも、 Rustでは利用可能なことを確かめました。これらのパターンには、異なる代償があります。 あなたが、オブジェクト指向のパターンには非常に馴染み深い可能性があるものの、問題を再考してRustの機能の強みを活かすと、 コンパイル時に一部のバグを回避できるなどの利益が得られることもあります。オブジェクト指向のパターンは、 オブジェクト指向言語にはない所有権などの特定の機能によりRustでは、必ずしも最善の解決策ではないでしょう。
まとめ
この章読了後に、あなたがRustはオブジェクト指向言語であると考えるかどうかに関わらず、 もうトレイトオブジェクトを使用してRustでオブジェクト指向の機能の一部を得ることができると知っています。 ダイナミックディスパッチは、多少の実行時性能と引き換えにコードに柔軟性を齎してくれます。 この柔軟性を利用してコードのメンテナンス性に寄与するオブジェクト指向パターンを実装することができます。 Rustにはまた、オブジェクト指向言語にはない所有権などの他の機能もあります。オブジェクト指向パターンは、 必ずしもRustの強みを活かす最善の方法にはなりませんが、利用可能な選択肢の1つではあります。
次は、パターンを見ます。パターンも多くの柔軟性を可能にするRustの別の機能です。 本全体を通して僅かに見かけましたが、まだその全能力は目の当たりにしていません。さあ、行きましょう!