Rustの列挙型
列挙型(enum、enumeration の略)は、「決まった選択肢のうちどれかひとつ」になる型を定義するものです。構造体が「このデータは、これらのフィールドを全部持っている」を表すのに対して、列挙型は「このデータは、これらの選択肢のうちちょうどひとつ」を表します。
列挙型を定義して使う
Section titled “列挙型を定義して使う”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 が漏れなく網羅されていると判断できるからです。
列挙型はデータを持てる
Section titled “列挙型はデータを持てる”ここが、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.2720.00Shape::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" と打ち間違えても誰も止めてくれません。それに、種類ごとにどのフィールドが意味を持つのかを、自分で確かめる必要も出てきます。列挙型なら、ありえない組み合わせはそもそも書けませんし、すべてのバリアントを処理するようコンパイラが求めてくれます。
1. 硬貨を数える
Section titled “1. 硬貨を数える”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セント2. 三角形を追加する
Section titled “2. 三角形を追加する”このページの列挙型 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.2818.0012.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 °C100.0 °C10.0 °Cself は借用されているので、c と f は参照(&f64)です。計算は参照のままでもできますが、摂氏の値そのものを返すには *c と書きます。これは「c が指している数値」という意味です。
- 列挙型は、名前の付いたいくつかのバリアントのうちひとつ、という型を定義する:
enum Direction { North, South, ... }。 - バリアントは自分のデータを持てる。バリアントごとに違う種類のデータを持たせてもよい。
matchを使うと列挙型のデータを自然に取り出せる。また、すべてのバリアントを処理することが求められる。- 構造体と同じように、列挙型も
implブロックでメソッドを持てる。 Option(と、このあと学ぶResult)は標準ライブラリの列挙型にすぎず、魔法のようなものではない。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. ほかの言語のよくある enum と違って、Rust の列挙型のバリアントにはどんなことができますか?
2. Direction の match の例で、残りを受け止める _ のアームが必要ないのはなぜですか?
3. 標準ライブラリで、Option<T> は実際には何として実装されていますか?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- Enumを定義する — The Rust Programming Language 日本語版:データを持つバリアント、列挙型のメソッド、そしてRustにnullがなく
Optionがある理由について。 - match制御フロー構造 — The Rust Programming Language 日本語版:列挙型に対する
matchをくわしく解説しています。硬貨を仕分ける例や、残りすべてを受け止めるパターンも出てきます。 - 列挙型 — Rust By Example 日本語版:
Click { x: i64, y: i64 }のような名前付きフィールドを持つバリアントも含めて、実際に動かせる例があります。 - Enumerations — The Rust Reference(英語):バリアントに明示的な数値を割り当てる方法など、細かいルールがすべて載っています。
Option— 標準ライブラリのドキュメント(英語):いちばんよく使うことになる列挙型の本当の定義と、便利なメソッドの一覧です。