コンテンツにスキップ

Rustの列挙型

列挙型(enum、enumeration の略)は、「決まった選択肢のうちどれかひとつ」になる型を定義するものです。構造体が「このデータは、これらのフィールドを全部持っている」を表すのに対して、列挙型は「このデータは、これらの選択肢のうちちょうどひとつ」を表します。

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("北へ向かっています"),
Direction::South => println!("南へ向かっています"),
Direction::East => println!("東へ向かっています"),
Direction::West => println!("西へ向かっています"),
}
}

実行結果:

北へ向かっています
▶ 実際に試してみよう

この match には、残りをすべて受け止める _ のアームがありません。Direction のとりうる値がすべて書かれているので、Rust はこの match が漏れなく網羅されていると判断できるからです。

ここが、Rust の列挙型がほかの言語のよくある enum より一歩進んでいるところです。それぞれのバリアント(variant、選択肢のひとつひとつ)が、自分専用のデータを持てます。

enum Shape {
Circle(f64), // 半径
Rectangle(f64, f64), // 幅、高さ
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

実行結果:

28.27
20.00
▶ 実際に試してみよう

Shape::Circle は f64 をひとつ(半径)、Shape::Rectangle は2つ持っています。match はそれぞれのアームで、そのデータをそのまま取り出してくれます。中身を「取り出す」ための別の手順は必要ありません。

列挙型にもメソッドを持たせられる

Section titled “列挙型にもメソッドを持たせられる”

構造体と同じように、列挙型にも impl ブロックでメソッドを持たせられます。メソッドの中では、たいてい match self を使って、バリアントごとに何をするかを決めます。

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?}: {}秒", light, light.seconds());
light = light.next();
}
}

実行結果:

Red: 30秒
Green: 25秒
Yellow: 3秒
Red: 30秒
▶ 実際に試してみよう

next はまったく新しい Light を返し、light = light.next(); で古いものと入れ替えています。だから light は mut で宣言しているのです。#[derive(Debug)] は構造体と同じように列挙型にも使えるので、{:?} でバリアントの名前が表示されます。

ロジックをメソッドにまとめておくと、型のすぐそばに置いておけます。Light を使う人は、時間がどう決まるのかを知らなくても light.seconds() を呼ぶだけで済みます。それに、あとで FlashingYellow のような新しいバリアントを追加したら、直す必要のある match をコンパイラがすべて指摘してくれます。

Option:じつはもう使っている列挙型

Section titled “Option:じつはもう使っている列挙型”

専用のページでくわしく学ぶ Option 型は、標準ライブラリで定義されたただの列挙型です。

enum Option<T> {
Some(T),
None,
}

列挙型がわかってしまえば、Option は特別な構文には見えなくなります。バリアントが2つあって、そのうちひとつが値を持っているだけの、ごくふつうの列挙型です。

「種類」フィールドを持つ構造体ではだめなの?

Section titled “「種類」フィールドを持つ構造体ではだめなの?”

構造体に "circle" や "rectangle" のような文字列のフィールドを持たせて、同じことをまねする方法も考えられます。でもそれだと、うっかり "circl" と打ち間違えても誰も止めてくれません。それに、種類ごとにどのフィールドが意味を持つのかを、自分で確かめる必要も出てきます。列挙型なら、ありえない組み合わせはそもそも書けませんし、すべてのバリアントを処理するようコンパイラが求めてくれます。

Penny、Nickel、Dime、Quarter というバリアントを持つ列挙型 Coin を作ってください。fn value_in_cents(coin: &Coin) -> u32 を書き、財布の中の硬貨を合計します。中身は25セント、10セント、5セント、1セント、もう1枚の25セントです。ヒント:硬貨は配列に入れて、&purse でループします。

解答を見る
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("合計: {total}セント");
}

実行結果:

合計: 66セント
▶ 実際に試してみよう

このページの列挙型 Shape に、3辺の長さを持つ Triangle バリアントを追加してください。それから、どの図形にも使える perimeter 関数(周の長さ)を書きます。ヒント:円の周の長さは 2.0 * 3.14159 * radius です。三角形は3辺を足したものです。

解答を見る
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

実行結果:

6.28
18.00
12.00
▶ 実際に試してみよう

試しに match から Shape::Triangle のアームを消してみてください。match がすべてのバリアントを網羅しなくなるので、Rustはコンパイルを拒否します。ケースの書き忘れがないよう、コンパイラが確かめてくれているのです。

Celsius と Fahrenheit という、どちらも f64 を1つ持つ2つのバリアントで列挙型 Temperature を作ってください。メソッド to_celsius(&self) -> f64 を追加し、Celsius(25.0)、Fahrenheit(212.0)、Fahrenheit(50.0) を小数点以下1桁の摂氏(°C)で表示します。ヒント:変換には (f - 32.0) * 5.0 / 9.0 を使います。

解答を見る
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

実行結果:

25.0 °C
100.0 °C
10.0 °C
▶ 実際に試してみよう

self は借用されているので、c と f は参照(&f64)です。計算は参照のままでもできますが、摂氏の値そのものを返すには *c と書きます。これは「c が指している数値」という意味です。

  • 列挙型は、名前の付いたいくつかのバリアントのうちひとつ、という型を定義する:enum Direction { North, South, ... }。
  • バリアントは自分のデータを持てる。バリアントごとに違う種類のデータを持たせてもよい。
  • match を使うと列挙型のデータを自然に取り出せる。また、すべてのバリアントを処理することが求められる。
  • 構造体と同じように、列挙型も impl ブロックでメソッドを持てる。
  • Option(と、このあと学ぶ Result)は標準ライブラリの列挙型にすぎず、魔法のようなものではない。

理解度チェック

1. ほかの言語のよくある enum と違って、Rust の列挙型のバリアントにはどんなことができますか?

2. Direction の match の例で、残りを受け止める _ のアームが必要ないのはなぜですか?

3. 標準ライブラリで、Option<T> は実際には何として実装されていますか?

スコア: 0 / 3