Enum と match で選択肢を表現する

RustBeginner
オンラインで実践に進む

はじめに

struct は、常に一緒に扱うフィールドをまとめます。一方、enum は、複数の選択肢のいずれかになる値を表し、それぞれの選択肢に異なるデータを持たせることができます。Rust の match 式を使うと、これらの選択肢を明示的に処理できます。

この実験では、メッセージを表す enum を完成させ、1 つのバリアントから座標を分解して取り出し、新しいバリアントを追加してモデルを拡張します。メッセージ処理を実行する前に、可能なすべてのバリアントを網羅する必要がある理由を、コンパイラーのエラーから確認します。

データを保持するバリアントを定義する

このステップでは、1 つの Message 型に対して、3 種類の選択肢を定義します。

用意されているプロジェクトに移動し、ソースファイルを開きます。

cd /home/labex/project/message-processor
nano src/main.rs

enum の宣言では、中括弧の中に バリアント を列挙します。コードからバリアントを作成するときは、enum の名前、::、バリアント名の順に記述します。同じ enum のバリアントごとに、異なる種類のデータを持たせることができます。

Step 1 のコメントを次の内容に置き換えます。

    Text(String),
    Move { x: i32, y: i32 },
    Stop,

Text(String) は名前のない値を 1 つ保持します。Rust では、これを タプル型バリアント と呼びます。Move { x, y } は名前付きフィールドを保持するため、構造体型バリアント と呼びます。Stop は追加のデータを持たないため、ユニットバリアント と呼びます。まずは正式名称を暗記するよりも、「名前のない値が 1 つ、名前付きフィールド、データなし」という形を理解することが重要です。形は異なりますが、作成された値はすべて同じ Message 型になります。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。用意されているプロセッサーをチェックして実行します。

cargo check
cargo run --quiet

出力は次のようになります。

Text: Maintenance at 9
Move received
Stop requested

用意されている match は、各バリアントに対応するアームを 1 つずつ選択します。各アームは パターン => 動作 の形式で記述し、短いアームはカンマで区切ります。Text(text) パターンは、保持されている文字列に名前を付けます。Move アームでは x と y にも名前を付け、一時的に (x, y) としてまとめています。ここでの括弧は、用意されている足場コードでのみ使用する 2 値のタプルを作成します。この Lab の学習目標はタプルではありません。_coordinates のように名前の先頭に _ を付けると、この一時的な値を使わないことが意図的であると Rust に伝えられます。これにより、最初の実行で警告が表示されません。次のステップでは、この足場コードを置き換え、両方の座標を出力に表示します。

バリアントのデータを分解して使う

このステップでは、Move の match アームを変更し、パターンで取り出した座標を出力に表示します。

パターンを使うと、マッチしたバリアントの中身を分解できます。構造体型バリアントでフィールド名を記述すると、アームの式から使用できる束縛が作成されます。

ソースファイルを開きます。

nano src/main.rs

Move アーム全体を次の内容に置き換えます。

        Message::Move { x, y } => println!("Move to ({x}, {y})"),

Move メッセージを受け取ると、パターンによって保持されているフィールドがローカル名 x と y に束縛されます。出力式ではこれらの名前をそのまま使用できるため、別途フィールドにアクセスしたり、キャストしたりする必要はありません。

保存して終了し、次のコマンドを実行します。

cargo run --quiet

中央の行が次のようになっていれば成功です。

Move to (3, -2)

y の値が負数になっていることから、固定した座標を出力したのではなく、実際のバリアントが保持しているデータをアームで使用したことが分かります。

網羅的な match を拡張する

このステップでは、Pause メッセージを追加し、コンパイラーによる網羅性チェックを確認してから、新しい選択肢を処理します。

match は、enum が取り得るすべての値を処理しなければなりません。この規則により、バリアントを追加すると、その新しいケースへの対応が必要なすべての match を特定できます。

ファイルを開き、Stop のすぐ下に次のバリアントを追加します。

nano src/main.rs
    Pause(u32),

保存して終了し、プログラムをチェックします。ここでは失敗するのが正しい動作です。

cargo check

診断メッセージの次の部分を確認します。

error[E0004]: non-exhaustive patterns: `Message::Pause(_)` not covered

enum に Pause の値を格納できるようになった一方で、その値をどう処理するかを示すアームがないため、コンパイラーは match message を指摘しています。

もう一度ソースファイルを開きます。Stop アームの下に次のアームを追加します。

        Message::Pause(seconds) => println!("Pause for {seconds} seconds"),

続いて、main にある用意された process(Message::Pause(5)); 行の先頭から // を削除します。パターンによって、保持されている u32 の値にローカル名 seconds が付けられます。

保存して終了し、チェックと実行を行います。

cargo check
cargo run --quiet

完全な出力は次のようになります。

Text: Maintenance at 9
Move to (3, -2)
Stop requested
Pause for 5 seconds

チェックが成功したことで、match が再び網羅的になったことを確認できます。また、4 行目の出力から、新しいアームが Pause の時間を分解して取り出したことが分かります。

まとめ

ユニット型、タプル型、構造体型の enum バリアントを定義し、match パターンで保持データを分解して取り出しました。また、新しい選択肢を追加した後、コンパイラーエラー E0004 を利用して match を網羅的に拡張しました。