enumを定義する
構造体は、width と height を持つ Rectangle のように、関連する
フィールドやデータをひとまとめにする方法を提供します。一方、enum は、
ある値が取りうる値の集合のうちの1つであることを表現する方法を提供しま
す。たとえば、Rectangle が、Circle や Triangle も含む図形の候補
の集合の1つである、と表したいことがあります。これを行うために、Rust で
はこれらの可能性を enum としてエンコードできます。
コードで表現したくなる場面を見て、この場合になぜ enum が便利で、構造体 よりも適しているのかを確認しましょう。IPアドレスを扱う必要があるとしま す。現在、IPアドレスには主要な標準が2つあります。バージョン4とバージョ ン6です。プログラムが遭遇しうる IPアドレスの可能性はこの2つしかないの で、取りうるすべてのバリアントを 列挙 できます。enumeration という名 前はここから来ています。
どの IPアドレスも、バージョン4のアドレスかバージョン6のアドレスのどち らかであり、同時に両方であることはありません。IPアドレスのこの性質によ って、enum というデータ構造が適切になります。なぜなら、enum の値はその バリアントのうち1つにしかなれないからです。バージョン4のアドレスもバー ジョン6のアドレスも、根本的にはどちらも IPアドレスです。そのため、コー ドがあらゆる種類の IPアドレスに当てはまる状況を扱うときには、同じ型と して扱うべきです。
この概念は、IpAddrKind という enum を定義し、IPアドレスが取りうる種
類として V4 と V6 を列挙することで、コードで表現できます。これらが
enum のバリアントです。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
これで IpAddrKind は、コードのほかの場所で使えるカスタムデータ型にな
りました。
enumの値
IpAddrKind の2つのバリアントそれぞれのインスタンスは、次のように作成
できます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
enum のバリアントはその識別子の名前空間の下にあり、その2つを区切るため
に二重コロンを使う点に注目してください。これは便利です。というのも、両
方の値 IpAddrKind::V4 と IpAddrKind::V6 は、同じ型 IpAddrKind の
値だからです。そのため、たとえば任意の IpAddrKind を受け取る関数を定
義できます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
そして、この関数はどちらのバリアントでも呼び出せます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
enum を使うことには、さらに多くの利点があります。私たちの IPアドレス型 についてもう少し考えると、現時点では実際の IPアドレスの データ を保存 する方法がありません。わかっているのは、それがどの 種類 かだけです。 5章で構造体について学んだばかりなので、リスト6-1に示すように、この問題 を構造体で解決したくなるかもしれません。
fn main() {
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
ここでは、2つのフィールドを持つ IpAddr 構造体を定義しています。1つは
型が IpAddrKind(先ほど定義した enum)である kind フィールド、もう
1つは型が String である address フィールドです。この構造体のインス
タンスは2つあります。1つ目は home で、kind には
IpAddrKind::V4 の値が入り、関連付けられたアドレスデータとして
127.0.0.1 を持ちます。2つ目のインスタンスは loopback です。こちら
は kind の値として IpAddrKind のもう一方のバリアントである V6 を
持ち、アドレス ::1 が関連付けられています。kind と address の値
をひとまとめにするために構造体を使ったので、これでバリアントが値に関連
付けられるようになりました。
しかし、同じ概念を enum だけで表すほうが、より簡潔です。構造体の中に
enum を入れるのではなく、各 enum バリアントに直接データを持たせられま
す。IpAddr enum のこの新しい定義では、V4 と V6 の両方のバリアン
トが関連する String 値を持つことになります。
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
データを各 enum バリアントに直接結び付けているので、余分な構造体は必要
ありません。ここでは、enum の動作に関するもう1つの詳細もわかりやすく見
て取れます。つまり、定義した各 enum バリアントの名前は、その enum のイ
ンスタンスを構築する関数にもなるということです。言い換えると、
IpAddr::V4() は String 引数を受け取り、IpAddr 型のインスタンスを
返す関数呼び出しです。enum を定義した結果として、このコンストラクタ関数
が自動的に定義されます。
構造体ではなく enum を使うことには、もう1つ利点があります。各バリアン
トは、関連付けられるデータの型や量をそれぞれ変えられるのです。バージョ
ン4の IPアドレスは、常に0から255までの値を取る4つの数値コンポーネント
を持ちます。もし V4 アドレスを4つの u8 値として保存しつつ、V6 ア
ドレスは1つの String 値として表したいなら、構造体ではそれはできませ
ん。enum はこのケースを簡単に扱えます。
fn main() {
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}
ここまで、バージョン4とバージョン6の IPアドレスを保存するためのデータ
構造を定義するいくつかの異なる方法を示してきました。しかし実際のところ、
IPアドレスを保存し、その種類を表現したいという要件は非常によくあるた
め、標準ライブラリには使える定義が用意されています!
標準ライブラリが IpAddr をどのように定義しているかを見てみましょう。
そこには、私たちが定義して使ってきたものとまったく同じ enum とバリアン
トがありますが、アドレスデータは2つの異なる構造体の形でバリアントの内
部に埋め込まれており、それぞれのバリアントごとに異なる定義になっていま
す。
#![allow(unused)]
fn main() {
struct Ipv4Addr {
// --省略--
}
struct Ipv6Addr {
// --省略--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
}
このコードは、enum のバリアントの中にどんな種類のデータでも入れられる ことを示しています。たとえば、文字列、数値型、構造体などです。別の enum を含めることさえできます。また、標準ライブラリの型は、たいてい あなたが思いつくものと比べてそれほど複雑ではありません。
標準ライブラリに IpAddr の定義が含まれているにもかかわらず、競合せず
に独自の定義を作成して使えることに注意してください。これは、標準ライブ
ラリの定義をまだ自分たちのスコープに持ち込んでいないからです。型をスコ
ープに持ち込むことについては、7章でさらに詳しく説明します。
リスト6-2にある enum の別の例を見てみましょう。こちらは、バリアントに さまざまな型が埋め込まれています。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
この enum には、型の異なる4つのバリアントがあります。
Quit: 関連付けられたデータをまったく持たないMove: 構造体のように名前付きフィールドを持つWrite: 単一のStringを含むChangeColor: 3つのi32値を含む リスト6-2のようなバリアントを持つ列挙型の定義は、さまざまな種類の構造体定義を 行うのと似ています。ただし、列挙型ではstructキーワードを使わず、すべての バリアントがMessage型の下にひとまとめにされます。次の構造体は、前述の列挙型の バリアントが保持するのと同じデータを保持できます。
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
fn main() {}
しかし、それぞれが独自の型を持つこれらの異なる構造体を使うと、リスト6-2で定義した
Message 列挙型のように、単一の型としてこれらのあらゆる種類のメッセージを受け取る
関数を簡単には定義できません。
列挙型と構造体には、もう1つ似ている点があります。impl を使って構造体にメソッドを
定義できるのと同じように、列挙型にもメソッドを定義できます。以下は、Message
列挙型に定義できる call という名前のメソッドです。
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
このメソッドの本体では、self を使って、そのメソッドを呼び出した値を取得します。
この例では、Message::Write(String::from("hello")) という値を持つ変数 m を作成
しており、m.call() が実行されると、call メソッドの本体で self はその値に
なります。
次に、標準ライブラリにある、非常に一般的で便利な別の列挙型 Option を見てみましょう。
Option 列挙型
この節では、標準ライブラリで定義されている別の列挙型である Option のケース
スタディを見ていきます。Option 型は、値が何かである場合もあれば、何もない
場合もあるという、非常によくある状況を表現します。
たとえば、空でないリストの最初の要素を要求すれば、値が得られます。空のリストの 最初の要素を要求すれば、何も得られません。この概念を型システムで表現することで、 コンパイラは、処理すべきすべてのケースをあなたが処理したかどうかを検査できます。 この機能は、他のプログラミング言語で非常によくあるバグを防ぐことができます。
プログラミング言語設計は、どの機能を含めるかという観点で考えられることが多い ですが、除外する機能も同様に重要です。Rust には、多くの他の言語にある null 機能がありません。Null とは、そこに値が存在しないことを意味する値です。null を持つ言語では、変数は常に null か null ではないかの2つの状態のいずれかに なり得ます。
2009年の講演「Null References: The Billion Dollar Mistake」で、null の発明者 である Tony Hoare は次のように述べています。
私はこれを、自分の10億ドルの失敗だと呼んでいます。当時、私はオブジェクト指向 言語における参照のための、最初の包括的な型システムを設計していました。私の 目標は、参照のあらゆる利用が絶対に安全であり、コンパイラが自動的に検査を 行うようにすることでした。しかし、あまりに実装が簡単だったため、null 参照を 入れたいという誘惑に抗えませんでした。その結果、数え切れないほどのエラー、 脆弱性、システムクラッシュを招き、この40年間でおそらく10億ドル分の苦痛と 損害を引き起こしてきました。
null 値の問題は、null 値を null ではない値として使おうとすると、何らかの 種類のエラーが発生することです。この null か null ではないかという性質は いたるところに関わるため、この種のエラーは非常に起こしやすいのです。
しかし、null が表現しようとしている概念自体は、依然として有用です。null は、 何らかの理由で現在は無効であるか、存在しない値です。
問題は、実際には概念そのものではなく、その特定の実装にあります。そのため、
Rust には null はありませんが、値が存在するか存在しないかという概念を表現
できる列挙型があります。この列挙型は Option<T> であり、標準ライブラリで
次のように定義されています。
#![allow(unused)]
fn main() {
enum Option<T> {
None,
Some(T),
}
}
Option<T> 列挙型は非常に有用なので、prelude にも含まれています。これを
明示的にスコープに導入する必要はありません。そのバリアントも prelude に含まれて
います。Option:: 接頭辞なしで Some と None を直接使えます。Option<T>
列挙型は、依然としてただの通常の列挙型であり、Some(T) と None は依然として
Option<T> 型のバリアントです。
<T> 構文は、Rust の機能の1つで、まだ説明していません。これはジェネリックな
型パラメータであり、ジェネリクスについては第10章でより詳しく扱います。今の
ところ知っておくべきなのは、<T> は Option 列挙型の Some バリアントが
任意の型のデータを1つ保持できること、そして T の代わりに使われる具体的な
型ごとに、全体の Option<T> 型も別の型になるということです。以下は、Option
値を使って数値型や char 型を保持する例です。
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}
some_number の型は Option<i32> です。some_char の型は Option<char> で、
これは別の型です。Rust は、Some バリアントの中に値を指定しているため、これらの
型を推論できます。absent_number については、Rust では全体の Option 型を
注釈する必要があります。コンパイラは、None の値だけを見ても、それに対応する
Some バリアントがどの型を保持するのかを推論できません。ここでは、Rust に
absent_number が Option<i32> 型であることを伝えています。
Some の値があるとき、値が存在することがわかり、その値は Some の中に保持
されています。None の値があるとき、それはある意味では null と同じことを
意味します。つまり、有効な値を持っていないということです。では、Option<T> を
持つことは、null を持つことより、なぜよいのでしょうか。
要するに、Option<T> と T(ここで T は任意の型になり得ます)は異なる型で
あるため、コンパイラは Option<T> の値を、あたかもそれが確実に有効な値である
かのように使うことを許さないからです。たとえば、次のコードはコンパイルされ
ません。i8 と Option<i8> を足そうとしているからです。
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
このコードを実行すると、次のようなエラーメッセージが表示されます。
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
強烈ですね! 実際のところ、このエラーメッセージが意味しているのは、Rust は
i8 と Option<i8> の足し方を理解していない、ということです。なぜなら、
それらは異なる型だからです。Rust で i8 のような型の値を持っているとき、
コンパイラはその値が常に有効であることを保証します。私たちは、その値を使う前に
null かどうかを確認しなくても、自信を持って先に進めます。Option<i8>(または
扱っている値の型が何であれ)を持っているときにだけ、値を持っていない可能性を
心配する必要があり、コンパイラは、その値を使う前にそのケースを確実に処理させます。
言い換えると、Option<T> を使って T の操作を実行する前に、Option<T> を T に変換する必要があります。一般に、これは null に関する最も一般的な問題の 1 つ、つまり実際には null であるのに null ではないと思い込んでしまうことを防ぐのに役立ちます。
null ではない値だと誤って思い込むリスクを取り除くことで、コードに対してより自信を持てるようになります。null である可能性のある値を持つには、その値の型を Option<T> にすることで、明示的にそうすることを選ばなければなりません。すると、その値を使うときには、その値が null である場合を明示的に処理することが求められます。値の型が Option<T> ではないあらゆる場所では、その値が null ではないと安全に仮定できます。これは、null が至る所に広がることを抑え、Rust コードの安全性を高めるための、Rust における意図的な設計判断でした。
では、Option<T> 型の値を持っているときに、その値を使えるようにするためには、Some バリアントからどのようにして T の値を取り出せばよいのでしょうか。Option<T> enum には、さまざまな状況で役立つ多数のメソッドがあります。詳しくは そのドキュメント を確認してください。Option<T> のメソッドに慣れ親しむことは、Rust を学んでいくうえで非常に役立ちます。
一般に、Option<T> の値を使うには、それぞれのバリアントを処理するコードを用意したいものです。Some(T) の値があるときにだけ実行されるコードが必要で、そのコードでは内部の T を使うことができます。None の値がある場合にだけ実行される別のコードも必要で、そのコードでは利用できる T の値はありません。match 式は、enum とともに使うとまさにこれを行う制御フロー構文です。enum のどのバリアントを持っているかに応じて異なるコードを実行し、そのコードは一致した値の内部にあるデータを使うことができます。