// book chapter
動かして学ぶRustプログラミング問題集第11章 列挙型とmatch式.
第10章までで、構造体にデータをまとめ、implブロックで振る舞いを与えられるようになりました。
この章では、もう1つのユーザー定義型である列挙型と、それを扱うためのmatch式を10問で身につけます。
構造体が「名前も価格も在庫も持つ」というAもBもCもの型だったのに対し、列挙型は「支払いは現金かカードかQR決済のどれか」というAかBかCを表す型です。取りうる選択肢を型として書き切ってしまうのが列挙型の役割で、そこにmatch式を組み合わせると「どの選択肢を処理し忘れているか」をコンパイラが指摘してくれるようになります。
この「書き忘れたら怒られる」という性質は、次の第12章で扱うOption型やResult型——Rustがnullや例外の代わりに使う仕組み——を支える土台でもあります。この章は、その手前にある一番大事な準備運動です。
進め方は第10章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 列挙型を定義する
列挙型に関する問題です。
3種類のドリンクを表す列挙型Drinkを定義してください。{:?}で出力できるようにする必要があります。
1杯目: Coffee
2杯目: Tea
3杯目: Juice// Coffee・Tea・Juiceの3つのバリアントを持つ列挙型Drinkを定義せよ
// {:?}で出力できるようにすること
fn main() {
let first = Drink::Coffee;
let second = Drink::Tea;
let third = Drink::Juice;
println!("1杯目: {first:?}");
println!("2杯目: {second:?}");
println!("3杯目: {third:?}");
}Playgroundで開く解答例と解説
// Coffee・Tea・Juiceの3つのバリアントを持つ列挙型Drinkを定義せよ
// {:?}で出力できるようにすること
#[derive(Debug)]
enum Drink {
Coffee,
Tea,
Juice,
}
fn main() {
let first = Drink::Coffee;
let second = Drink::Tea;
let third = Drink::Juice;
println!("1杯目: {first:?}");
println!("2杯目: {second:?}");
println!("3杯目: {third:?}");
}Playgroundで開くenum 型名 { ... }と書き、中に取りうる選択肢を並べたものが列挙型です。並べた1つ1つをバリアントと呼びます。
構造体との違いは、値の作り方を見るとはっきりします。
| 構造体 | 列挙型 | |
|---|---|---|
| 意味 | すべてのフィールドを同時に持つ | どれか1つの状態を取る |
| 定義 | struct Product { name, price } |
enum Drink { Coffee, Tea, Juice } |
| 値の作り方 | Product { name: ..., price: ... } |
Drink::Coffee |
バリアントはDrink::Coffeeのように型名::バリアント名で書きます。Drink::CoffeeとDrink::Teaはどちらも型としてはDrinkで、変数に入れたり関数の引数にしたりできます。
#[derive(Debug)]は第9章の構造体のときと同じで、{:?}での出力をコンパイラに自動生成させる指定です。列挙型でも付け方はまったく同じで、{:?}で出力するとバリアント名がそのまま表示されます。
02 - match式で分岐する
match式と列挙型に関する問題です。
announce関数の中身を書いて、バリアントごとにメッセージを出し分けてください。
コーヒーをどうぞ
ジュースをどうぞ
紅茶をどうぞenum Drink {
Coffee,
Tea,
Juice,
}
fn announce(drink: Drink) {
// match式でバリアントごとに次のメッセージを出力せよ
// Coffee → コーヒーをどうぞ
// Tea → 紅茶をどうぞ
// Juice → ジュースをどうぞ
}
fn main() {
announce(Drink::Coffee);
announce(Drink::Juice);
announce(Drink::Tea);
}Playgroundで開く解答例と解説
enum Drink {
Coffee,
Tea,
Juice,
}
fn announce(drink: Drink) {
// match式でバリアントごとに次のメッセージを出力せよ
// Coffee → コーヒーをどうぞ
// Tea → 紅茶をどうぞ
// Juice → ジュースをどうぞ
match drink {
Drink::Coffee => println!("コーヒーをどうぞ"),
Drink::Tea => println!("紅茶をどうぞ"),
Drink::Juice => println!("ジュースをどうぞ"),
}
}
fn main() {
announce(Drink::Coffee);
announce(Drink::Juice);
announce(Drink::Tea);
}Playgroundで開くmatch式は、対象の値を上から順にパターンと照合し、最初に一致した1つの処理だけを実行する構文です。パターン => 処理,という形の行をアームと呼びます。
match 対象の値 {
パターン1 => 処理1,
パターン2 => 処理2,
}書き方で押さえておくところは3つです。
- アームの区切りは
,(カンマ)。{}で囲んだブロックを書く場合はカンマを省略できます - 矢印は
=>(->ではありません) - C言語の
switchと違い、breakは不要です。一致したアームだけが実行され、次のアームへ流れ落ちることはありません
if式で書けないのかというと、if drink == Drink::Coffeeのような比較には別途準備が必要なうえ、選択肢が増えるほどelse ifが伸びていきます。列挙型の分岐はmatch式で書くのが基本形だと考えてください。
03 - matchは式
match式と式に関する問題です。
let price = 0;の行をmatch式に書き換えて、ドリンクに応じた価格をpriceに束縛してください。
お会計: 500円
お会計: 450円
お会計: 400円enum Drink {
Coffee,
Tea,
Juice,
}
fn checkout(drink: Drink) {
// match式で価格を求めてpriceに束縛せよ
// Coffee: 500円 / Tea: 450円 / Juice: 400円
let price = 0;
println!("お会計: {price}円");
}
fn main() {
checkout(Drink::Coffee);
checkout(Drink::Tea);
checkout(Drink::Juice);
}Playgroundで開く解答例と解説
enum Drink {
Coffee,
Tea,
Juice,
}
fn checkout(drink: Drink) {
// match式で価格を求めてpriceに束縛せよ
// Coffee: 500円 / Tea: 450円 / Juice: 400円
let price = 0;
let price = match drink {
Drink::Coffee => 500,
Drink::Tea => 450,
Drink::Juice => 400,
};
println!("お会計: {price}円");
}
fn main() {
checkout(Drink::Coffee);
checkout(Drink::Tea);
checkout(Drink::Juice);
}Playgroundで開くmatchはその名のとおり式です。第3章でlet x = if ... { ... } else { ... };と書けたのと同じで、一致したアームの値がmatch式全体の値になります。
let文の右辺に置いているので、末尾のセミコロンを忘れないでください。};の;はlet文を終わらせるためのものです。
式として使うときの決まりが1つあります。すべてのアームが同じ型の値を返すことです。
let price = match drink {
Drink::Coffee => 500,
Drink::Tea => "450円", // エラー: 他のアームは整数なのに文字列を返している
Drink::Juice => 400,
};priceの型は1つに決まらなければならないので、アームごとに違う型を返すことはできません。第3章のif式で分岐の型を揃える必要があったのと同じ理由です。
問題02のようにprintln!を並べる書き方と比べると、式として使う書き方には利点があります。
- 値を返すことに集中できる。「価格を決める」処理と「表示する」処理が分かれます
- 書き忘れをコンパイラが防いでくれる。次の問題04で扱います
04 - 網羅性チェック
match式と列挙型に関する問題です。
次のコードはコンパイルエラー(E0004)になります。1つアームを足して修正してください。
お会計: 500円
お会計: 450円
お会計: 400円enum Drink {
Coffee,
Tea,
Juice,
}
fn checkout(drink: Drink) {
let price = match drink {
Drink::Coffee => 500,
Drink::Tea => 450,
};
println!("お会計: {price}円");
}
fn main() {
checkout(Drink::Coffee);
checkout(Drink::Tea);
checkout(Drink::Juice);
}Playgroundで開く解答例と解説
enum Drink {
Coffee,
Tea,
Juice,
}
fn checkout(drink: Drink) {
let price = match drink {
Drink::Coffee => 500,
Drink::Tea => 450,
Drink::Juice => 400, // このアームを追加した
};
println!("お会計: {price}円");
}
fn main() {
checkout(Drink::Coffee);
checkout(Drink::Tea);
checkout(Drink::Juice);
}Playgroundで開くエラーメッセージはnon-exhaustive patterns: 'Drink::Juice' not covered(E0004)です。「パターンが網羅的でない、Drink::Juiceが処理されていない」と、どのバリアントを書き忘れたのかまで名指しで教えてくれます。
これがmatch式の最大の価値である網羅性チェックです。Rustのmatchは、対象の型が取りうる値をすべて処理していなければコンパイルを通しません。
なぜこれが重要なのでしょうか。列挙型は、開発を進める中でバリアントが増えていきます。
enum Drink {
Coffee,
Tea,
Juice,
Beer, // 新しく追加した
}このとき、Drinkをmatchしているコードがすべてコンパイルエラーになります。プログラムのどこかに対応漏れが残ったまま動いてしまう、ということが起こりません。他の言語のswitch文なら「気付かないまま何も起こらない分岐」になっていたところを、コンパイラがすべて洗い出してくれます。
05 - データを持つバリアント
列挙型とmatch式に関する問題です。
Discountedのアームを追加して、割引額を反映した金額を返してください。基本料金は500円です。
通常: 500円
割引: 380円enum Order {
Regular,
Discounted(u32), // 割引額(円)
}
fn total(order: Order) -> u32 {
match order {
Order::Regular => 500,
// Discountedのアームを追加せよ
// 割引額を取り出し、500から引いた金額を返すこと
}
}
fn main() {
println!("通常: {}円", total(Order::Regular));
println!("割引: {}円", total(Order::Discounted(120)));
}Playgroundで開く解答例と解説
enum Order {
Regular,
Discounted(u32), // 割引額(円)
}
fn total(order: Order) -> u32 {
match order {
Order::Regular => 500,
// Discountedのアームを追加せよ
// 割引額を取り出し、500から引いた金額を返すこと
Order::Discounted(amount) => 500 - amount,
}
}
fn main() {
println!("通常: {}円", total(Order::Regular));
println!("割引: {}円", total(Order::Discounted(120)));
}Playgroundで開くバリアントにはデータを持たせられます。Discounted(u32)のように括弧を付けて型を書くと、そのバリアントはu32の値を1つ抱えるようになります。第9章のタプル構造体と同じ書き方です。
値の作り方もタプル構造体と同じで、Order::Discounted(120)のように関数呼び出しの形で書きます。
そして取り出し方がmatchのパターンです。
Order::Discounted(amount) => 500 - amount,
// ^^^^^^ ここに書いた名前に、抱えている値が束縛されるamountは新しく作られる変数で、Discountedが持っているu32の値がそこに入ります。アームの右側(=>の後ろ)でだけ使えます。名前は自由に付けられるので、Order::Discounted(x)と書けばxという名前になります。
これが列挙型の強力なところです。「割引注文のときだけ割引額が存在する」という関係を型で表現できていて、しかも割引額を使えるのはDiscountedのアームの中だけです。Regularのアームで割引額を読もうとしても、そんな変数は存在しません。存在しない値をうっかり読むコードが書けない構造になっています。
各バリアントは、それぞれ違う個数・違う型のデータを持てます。
enum Payment {
Cash, // データなし
Card(String), // カード番号
Point(u32, String), // ポイント数と会員ID
}06 - 構造体的バリアント
列挙型とmatch式に関する問題です。
Shippingのアームを追加して、配送先と送料を出力してください。
店頭受け取りです
東京都渋谷区へ配送します(送料500円)enum Delivery {
Pickup,
Shipping { address: String, fee: u32 },
}
fn announce(delivery: Delivery) {
match delivery {
Delivery::Pickup => println!("店頭受け取りです"),
// Shippingのアームを追加せよ
// 「〇〇へ配送します(送料〇〇円)」と出力すること
}
}
fn main() {
announce(Delivery::Pickup);
announce(Delivery::Shipping {
address: String::from("東京都渋谷区"),
fee: 500,
});
}Playgroundで開く解答例と解説
enum Delivery {
Pickup,
Shipping { address: String, fee: u32 },
}
fn announce(delivery: Delivery) {
match delivery {
Delivery::Pickup => println!("店頭受け取りです"),
// Shippingのアームを追加せよ
// 「〇〇へ配送します(送料〇〇円)」と出力すること
Delivery::Shipping { address, fee } => {
println!("{address}へ配送します(送料{fee}円)");
}
}
}
fn main() {
announce(Delivery::Pickup);
announce(Delivery::Shipping {
address: String::from("東京都渋谷区"),
fee: 500,
});
}Playgroundで開くバリアントは{ ... }で名前付きフィールドを持つこともできます。定義も値の作り方も構造体そのままで、Delivery::Shipping { address: ..., fee: ... }と書きます。
パターンでの分解も構造体の作り方に似た形です。
Delivery::Shipping { address, fee } => { ... }
// ^^^^^^^ ^^^ フィールド名を書くと、同じ名前の変数に束縛される第9章のフィールド初期化省略記法と同じく、address: addressのような重複を書かずに済むようになっています。別の名前を付けたい場合はDelivery::Shipping { address: to, fee: cost }のようにフィールド名: 新しい名前と書きます。
バリアントが持つデータの形は、これで3種類すべてがそろいました。
| 形 | 定義 | パターン |
|---|---|---|
| データなし | Pickup |
Delivery::Pickup |
| タプル的 | Discounted(u32) |
Order::Discounted(amount) |
| 構造体的 | Shipping { address: String, fee: u32 } |
Delivery::Shipping { address, fee } |
どれを選ぶかの目安は、データが1つか2つで意味が明らかなら括弧の形、フィールドが増えて名前がないと分からなくなってきたら{}の形です。
07 - 包括パターン
包括パターン・match式・範囲式に関する問題です。
2つの関数のmatchに、それぞれ最後のアームを追加してください。describeでは貯まっているポイント数を出力し、is_free_drinkではポイント数を使いません。
ポイントはありません
あと少しで特典です
150ポイント貯まっています
無料ドリンク券は使えません
無料ドリンク券が使えますfn describe(points: u32) {
match points {
0 => println!("ポイントはありません"),
1..=99 => println!("あと少しで特典です"),
// 100以上をまとめて処理するアームを追加せよ
// 「〇〇ポイント貯まっています」と出力すること
}
}
fn is_free_drink(points: u32) {
match points {
100 => println!("無料ドリンク券が使えます"),
// それ以外をまとめて処理するアームを追加せよ
// 「無料ドリンク券は使えません」と出力すること(ポイント数は使わない)
}
}
fn main() {
describe(0);
describe(30);
describe(150);
is_free_drink(150);
is_free_drink(100);
}Playgroundで開く解答例と解説
fn describe(points: u32) {
match points {
0 => println!("ポイントはありません"),
1..=99 => println!("あと少しで特典です"),
// 100以上をまとめて処理するアームを追加せよ
// 「〇〇ポイント貯まっています」と出力すること
other => println!("{other}ポイント貯まっています"),
}
}
fn is_free_drink(points: u32) {
match points {
100 => println!("無料ドリンク券が使えます"),
// それ以外をまとめて処理するアームを追加せよ
// 「無料ドリンク券は使えません」と出力すること(ポイント数は使わない)
_ => println!("無料ドリンク券は使えません"),
}
}
fn main() {
describe(0);
describe(30);
describe(150);
is_free_drink(150);
is_free_drink(100);
}Playgroundで開くu32が取りうる値は約43億通りあるので、列挙型のようにすべてのアームを書き並べることはできません。そこで残り全部をまとめて引き受けるのが包括パターンです。書き方は2通りあります。
| 書き方 | 呼び名 | 値 |
|---|---|---|
other => |
識別子パターン | 変数otherに束縛され、アームの中で使える |
_ => |
ワイルドカードパターン | 捨てられる。アームの中では使えない |
値を使うなら識別子パターン、使わないなら_ という単純な使い分けです。_のところにotherと書いても動きますが、使っていない変数として警告が出ます。
1..=99は第3章で学んだ範囲の記法です。matchのパターンとしても書けて、1から99までのどれかに一致します。
問題04で「_でごまかすと網羅性チェックの利点が失われる」と書いたことと、今回_を使ったことは矛盾していません。判断の基準は次のとおりです。
- 列挙型を
matchするときは、_を避けてバリアントを1つずつ書く。バリアントが増えたときにコンパイラが漏れを教えてくれます - 数値や文字列のように値を列挙しきれない型では、
_や識別子パターンが必要です
08 - 複数パターンをまとめる
match式に関する問題です。
次のコードは正しく動きますが、同じ処理が2回ずつ書かれています。|を使ってアームを2つにまとめてください。出力は変わりません。
ホットもご用意できます
ホットもご用意できます
冷たいドリンクのみです
冷たいドリンクのみですenum Drink {
Coffee,
Tea,
Juice,
Water,
}
fn serve_hot(drink: Drink) {
match drink {
Drink::Coffee => println!("ホットもご用意できます"),
Drink::Tea => println!("ホットもご用意できます"),
Drink::Juice => println!("冷たいドリンクのみです"),
Drink::Water => println!("冷たいドリンクのみです"),
}
}
fn main() {
serve_hot(Drink::Coffee);
serve_hot(Drink::Tea);
serve_hot(Drink::Juice);
serve_hot(Drink::Water);
}Playgroundで開く解答例と解説
enum Drink {
Coffee,
Tea,
Juice,
Water,
}
fn serve_hot(drink: Drink) {
match drink {
Drink::Coffee => println!("ホットもご用意できます"),
Drink::Tea => println!("ホットもご用意できます"),
Drink::Juice => println!("冷たいドリンクのみです"),
Drink::Water => println!("冷たいドリンクのみです"),
Drink::Coffee | Drink::Tea => println!("ホットもご用意できます"),
Drink::Juice | Drink::Water => println!("冷たいドリンクのみです"),
}
}
fn main() {
serve_hot(Drink::Coffee);
serve_hot(Drink::Tea);
serve_hot(Drink::Juice);
serve_hot(Drink::Water);
}Playgroundで開く1つのアームに複数のパターンを書きたいときは|(パイプ)で区切ります。「CoffeeまたはTeaなら」という意味で、どれかに一致すればそのアームが実行されます。
||(論理和)ではなく|が1本である点に注意してください。第2章で学んだ論理演算子とは別物で、これはパターンの区切りです。
_で書いてしまうのとは何が違うのでしょうか。今回のコードは_ => println!("冷たいドリンクのみです")と書いても同じ出力になりますが、|で明示的に並べておくとバリアントが増えたときにコンパイラが教えてくれます。
enum Drink {
Coffee,
Tea,
Juice,
Water,
Beer, // 追加
}|で並べていればE0004になり、「ビールはホットで出すのか」を必ず判断させられます。_で書いていれば黙って「冷たいドリンクのみ」に分類され、誰も気付きません。問題07で触れた「列挙型では_を避ける」は、こういう場面のことです。
|は数値のパターンでも使えます。範囲と混ぜることもできます。
match day {
1 | 2 | 3 | 4 | 5 => println!("平日"),
6 | 7 => println!("週末"),
_ => println!("そんな曜日はありません"),
}09 - 中の値を使わないパターン
列挙型・match式・包括パターンに関する問題です。
DiscountedとBulkのアームを追加して、注文の種類だけを出力してください。どちらのアームでも中の値は使いません。
通常注文です
割引注文です
まとめ買いですenum Order {
Regular,
Discounted(u32), // 割引額(円)
Bulk { count: u32 }, // 個数
}
fn describe(order: Order) {
match order {
Order::Regular => println!("通常注文です"),
// DiscountedとBulkのアームを追加せよ
// Discounted → 割引注文です / Bulk → まとめ買いです
// どちらのアームでも中の値は使わないこと
}
}
fn main() {
describe(Order::Regular);
describe(Order::Discounted(120));
describe(Order::Bulk { count: 10 });
}Playgroundで開く解答例と解説
enum Order {
Regular,
Discounted(u32), // 割引額(円)
Bulk { count: u32 }, // 個数
}
fn describe(order: Order) {
match order {
Order::Regular => println!("通常注文です"),
// DiscountedとBulkのアームを追加せよ
// Discounted → 割引注文です / Bulk → まとめ買いです
// どちらのアームでも中の値は使わないこと
Order::Discounted(_) => println!("割引注文です"),
Order::Bulk { .. } => println!("まとめ買いです"),
}
}
fn main() {
describe(Order::Regular);
describe(Order::Discounted(120));
describe(Order::Bulk { count: 10 });
}Playgroundで開くまず、Order::Discounted => ...とバリアント名だけを書いてみたくなりますが、これはコンパイルエラーになります。
enum Order {
Regular,
Discounted(u32),
}
fn describe(order: Order) {
match order {
Order::Regular => println!("通常注文です"),
Order::Discounted => println!("割引注文です"), // エラー: 値を持つ形が書かれていない
}
}値を持つバリアントのパターンには、持っているデータの形まで書く必要があります。かといって問題05のようにOrder::Discounted(amount)と名前で束縛すると、今回は値を使わないので「使っていない変数」の警告が出てしまいます。そこで「中身には興味がない」と伝える専用の書き方を使います。
- タプル的バリアントは
Order::Discounted(_)と書きます。_は問題07で登場したワイルドカードパターンで、どんな値にも一致しつつ変数への束縛はしません。値を2つ持つバリアントなら(_, _)のように個数分書きます - 構造体的バリアントは
Order::Bulk { .. }と書きます。問題06のtipで「一部のフィールドだけ取り出す」ために使った..と同じもので、すべてのフィールドを省略すれば「中身をまったく見ない」という意味になります
3つの形をまとめると次のとおりです。
| バリアントの形 | 定義 | 中の値を使わないパターン |
|---|---|---|
| データなし | Regular |
Order::Regular |
| タプル的 | Discounted(u32) |
Order::Discounted(_) |
| 構造体的 | Bulk { count: u32 } |
Order::Bulk { .. } |
問題07の包括パターン(アーム全体を_にする書き方)との違いに注意してください。Order::Discounted(_)が無視しているのは中の値だけで、どのバリアントかの区別はしっかり残っています。アームはバリアントごとに並んだままなので、バリアントが増えればE0004で書き忘れを教えてくれるという網羅性チェックの利点も失われません。
10 - 応用: 図形の面積
この章の総復習として、列挙型・メソッド・match式を組み合わせた問題です。
図形を表す列挙型Shapeと、そのメソッドを実装してテストに合格させてください。
| バリアント | 持つデータ | 面積 |
|---|---|---|
Circle |
f64(半径) |
半径 × 半径 × 円周率 |
Rectangle |
width・height(f64) |
幅 × 高さ |
Triangle |
base・height(f64) |
底辺 × 高さ ÷ 2 |
メソッドは2つです。面積を返すareaと、図形の名前(円・長方形・三角形)をStringで返すname。
1つだけ新しい書き方が登場します。コードの1行目にあるuse std::f64::consts::PI;です。円周率のような定数は標準ライブラリにあらかじめ用意されていて、std::f64::consts::PIという「置き場所」まで含んだ長い名前で呼び出せます。use 置き場所の名前;とファイルの先頭に書いておくと、それ以降は末尾のPIとだけ書けば使えるようになります。円の面積の計算にはこのPIを使ってください。
use std::f64::consts::PI; // 円周率
// 図形を表す列挙型Shapeを定義せよ
// Circle … 半径をf64で1つ持つ(タプル的バリアント)
// Rectangle … width・heightをf64で持つ(構造体的バリアント)
// Triangle … base・heightをf64で持つ(構造体的バリアント)
// implブロックにareaとnameの2つのメソッドを定義せよ
#[test]
fn test_circle() {
let shape = Shape::Circle(2.0);
assert_eq!(shape.area(), PI * 2.0 * 2.0);
assert_eq!(shape.name(), "円");
}
#[test]
fn test_rectangle() {
let shape = Shape::Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(shape.area(), 12.0);
assert_eq!(shape.name(), "長方形");
}
#[test]
fn test_triangle() {
let shape = Shape::Triangle {
base: 6.0,
height: 4.0,
};
assert_eq!(shape.area(), 12.0);
assert_eq!(shape.name(), "三角形");
}
#[test]
fn test_total_area() {
let shapes = vec![
Shape::Rectangle {
width: 3.0,
height: 4.0,
},
Shape::Triangle {
base: 6.0,
height: 4.0,
},
];
let mut total = 0.0;
for shape in &shapes {
total += shape.area();
}
assert_eq!(total, 24.0);
}Playgroundで開く解答例と解説
use std::f64::consts::PI; // 円周率
// 図形を表す列挙型Shapeを定義せよ
// Circle … 半径をf64で1つ持つ(タプル的バリアント)
// Rectangle … width・heightをf64で持つ(構造体的バリアント)
// Triangle … base・heightをf64で持つ(構造体的バリアント)
enum Shape {
Circle(f64),
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
// implブロックにareaとnameの2つのメソッドを定義せよ
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(radius) => PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => base * height / 2.0,
}
}
fn name(&self) -> String {
match self {
Shape::Circle(_) => String::from("円"),
Shape::Rectangle { .. } => String::from("長方形"),
Shape::Triangle { .. } => String::from("三角形"),
}
}
}
#[test]
fn test_circle() {
let shape = Shape::Circle(2.0);
assert_eq!(shape.area(), PI * 2.0 * 2.0);
assert_eq!(shape.name(), "円");
}
#[test]
fn test_rectangle() {
let shape = Shape::Rectangle {
width: 3.0,
height: 4.0,
};
assert_eq!(shape.area(), 12.0);
assert_eq!(shape.name(), "長方形");
}
#[test]
fn test_triangle() {
let shape = Shape::Triangle {
base: 6.0,
height: 4.0,
};
assert_eq!(shape.area(), 12.0);
assert_eq!(shape.name(), "三角形");
}
#[test]
fn test_total_area() {
let shapes = vec![
Shape::Rectangle {
width: 3.0,
height: 4.0,
},
Shape::Triangle {
base: 6.0,
height: 4.0,
},
];
let mut total = 0.0;
for shape in &shapes {
total += shape.area();
}
assert_eq!(total, 24.0);
}Playgroundで開く列挙型にimplブロックを書けることに気付けたでしょうか。構造体とまったく同じ書き方で、メソッドも関連関数も定義できます。
「図形の種類ごとに面積の求め方が違う」という状況は、列挙型とmatchがぴったり合う典型例です。面積の計算式が3か所に散らばるのではなく、areaメソッドの中のmatchに集まります。図形の種類が増えたら、areaとnameがコンパイルエラーになって「両方に追記が必要だ」と教えてくれます。
いくつか押さえておきたい点があります。
use std::f64::consts::PI;は何をしているか
標準ライブラリの機能は、stdの中に「モジュール」という単位で階層的に整理されていて、::で区切った経路(パスと呼びます)で場所を指し示せます。円周率PIの置き場所は「stdの中のf64の中のconsts」です。毎回std::f64::consts::PIと書いても動きますが、useでパスを取り込んでおくと、それ以降は末尾のPIだけで参照できます。実際のRustコードでも、ファイルの先頭にuseを並べて必要な名前を取り込んでおくのが基本のスタイルです。
match selfと書けるのはなぜか
selfは&Shape(共有参照)なのに、パターンにはShape::Circle(radius)と参照でない形を書いています。これはコンパイラが自動で参照を辿ってくれるためで、束縛されるradiusのほうが&f64になります。PI * radius * radiusのように、そのまま計算に使えます。
_と..で「中身は見ない」と書く
nameメソッドでは、どのバリアントかだけが分かれば十分で、中のデータは使いません。問題09で学んだとおり、タプル的バリアントはShape::Circle(_)、構造体的バリアントはShape::Rectangle { .. }と書いて「中身には興味がない」ことを示します。
assert_eq!(shape.name(), "円")が通る理由
nameが返すのはString、比較相手は&strと、型が違います。それでも比較できるのは、標準ライブラリがStringと&strの比較をあらかじめ用意しているからです。
小数の比較には注意が必要
test_circleがassert_eq!(shape.area(), PI * 2.0 * 2.0)と、答えを直接書かずに同じ式で書いてあるのは意図的です。第2章で触れたとおりf64は誤差を持つため、3.14159265358979のような数値を書いても一致するとは限りません。実務では(a - b).abs() < 0.000001のように「十分近いか」で判定します。