Enumを定義する
コードで表現したくなるかもしれない場面に目を向けて、enumが有用でこの場合、構造体よりも適切である理由を確認しましょう。 IPアドレスを扱う必要が出たとしましょう。現在、IPアドレスの規格は二つあります: バージョン4とバージョン6です。 これらは、プログラムが遭遇するIPアドレスのすべての可能性です: 列挙型は、取りうる値をすべて列挙でき、 これが列挙型の名前の由来です。
どんなIPアドレスも、バージョン4かバージョン6のどちらかになりますが、同時に両方にはなり得ません。 IPアドレスのその特性により、enumデータ構造が適切なものになります。というのも、 enumの値は、その列挙子のいずれか一つにしかなり得ないからです。バージョン4とバージョン6のアドレスは、 どちらも根源的にはIPアドレスですから、コードがいかなる種類のIPアドレスにも適用される場面を扱う際には、 同じ型として扱われるべきです。
この概念をコードでは、IpAddrKind
列挙型を定義し、IPアドレスがなりうる種類、V4
とV6
を列挙することで、
表現できます。これらは、enumの列挙子として知られています:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
これで、IpAddrKind
はコードの他の場所で使用できる独自のデータ型になります。
Enumの値
以下のようにして、IpAddrKind
の各列挙子のインスタンスは生成できます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
enumの列挙子は、その識別子の元に名前空間分けされていることと、
2連コロンを使ってその二つを区別していることに注意してください。
これが有効な理由は、こうすることで、値IpAddrKind::V4
とIpAddrKind::V6
という値は両方とも、
同じ型IpAddrKind
になったからです。そうしたら、例えば、どんなIpAddrKind
を取る関数も定義できるようになります。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } }
そして、この関数をどちらの列挙子に対しても呼び出せます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } route(IpAddrKind::V4); route(IpAddrKind::V6); }
enumの利用には、さらなる利点さえもあります。このIPアドレス型についてもっと考えてみると、現状では、 実際のIPアドレスのデータを保持する方法がありません。つまり、どんな種類であるかを知っているだけです。 構造体について第5章で学んだばっかりとすると、この問題に対して、あなたはリスト6-1のように対処するかもしれません。
#![allow(unused)] 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"), }; }
ここでは、二つのフィールドを持つIpAddr
という構造体を定義しています: IpAddrKind
型(先ほど定義したenumですね)のkind
フィールドと、
String
型のaddress
フィールドです。この構造体のインスタンスが2つあります。最初のインスタンス、
home
にはkind
としてIpAddrKind::V4
があり、紐付けられたアドレスデータは127.0.0.1
です。
2番目のインスタンス、loopback
には、kind
の値として、IpAddrKind
のもう一つの列挙子、V6
があり、
アドレス::1
が紐付いています。構造体を使ってkind
とaddress
値を一緒に包んだので、
もう列挙子は値と紐付けられています。
各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、
同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddr
の定義は、
V4
とV6
列挙子両方にString
値が紐付けられていることを述べています。
#![allow(unused)] 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を使うことには、別の利点もあります: 各列挙子に紐付けるデータの型と量は、異なってもいいのです。
バージョン4のIPアドレスには、常に0から255の値を持つ4つの数値があります。V4
のアドレスは、4つのu8
型の値として格納するけれども、
V6
のアドレスは引き続き、単独のString
型の値で格納したかったとしても、構造体では不可能です。
enumなら、こんな場合も容易に対応できます:
#![allow(unused)] 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と列挙子がありますが、アドレスデータを二種の異なる構造体の形で列挙子に埋め込み、
この構造体は各列挙子用に異なる形で定義されています。
#![allow(unused)] fn main() { struct Ipv4Addr { // 省略 } struct Ipv6Addr { // 省略 } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
このコードは、enum列挙子内にいかなる種類のデータでも格納できることを描き出しています: 例を挙げれば、文字列、数値型、構造体などです。他のenumを含むことさえできます!また、 標準ライブラリの型は、あなたの想像するよりも複雑ではないことがしばしばあります。
標準ライブラリにIpAddr
に対する定義は含まれるものの、標準ライブラリの定義をまだ我々のスコープに導入していないので、
干渉することなく自分自身の定義を生成して使用できることに注意してください。型をスコープに導入することについては、
第7章でもっと詳しく言及します。
リスト6-2でenumの別の例を見てみましょう: 今回のコードは、幅広い種類の型が列挙子に埋め込まれています。
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } }
このenumには、異なる型の列挙子が4つあります:
Quit
には紐付けられたデータは全くなし。Move
は、中に匿名構造体を含む。Write
は、単独のString
オブジェクトを含む。ChangeColor
は、3つのi32
値を含む。
リスト6-2のような列挙子を含むenumを定義することは、enumの場合、struct
キーワードを使わず、
全部の列挙子がMessage
型の元に分類される点を除いて、異なる種類の構造体定義を定義するのと類似しています。
以下の構造体も、先ほどのenumの列挙子が保持しているのと同じデータを格納することができるでしょう:
#![allow(unused)] fn main() { struct QuitMessage; // ユニット構造体 struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // タプル構造体 struct ChangeColorMessage(i32, i32, i32); // タプル構造体 }
ですが、異なる構造体を使っていたら、各々、それ自身の型があるので、単独の型になるリスト6-2で定義したMessage
enumほど、
これらの種のメッセージいずれもとる関数を簡単に定義することはできないでしょう。
enumと構造体にはもう1点似通っているところがあります: impl
を使って構造体にメソッドを定義できるのと全く同様に、
enumにもメソッドを定義することができるのです。こちらは、Message
enum上に定義できるcall
という名前のメソッドです:
#![allow(unused)] 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
が表す値になります。
非常に一般的で有用な別の標準ライブラリのenumを見てみましょう: Option
です。
Option
enumとNull値に勝る利点
前節で、IpAddr
enumがRustの型システムを使用して、プログラムにデータ以上の情報をコード化できる方法を目撃しました。
この節では、Option
のケーススタディを掘り下げていきます。この型も標準ライブラリにより定義されているenumです。
このOption
型はいろんな箇所で使用されます。なぜなら、値が何かかそうでないかという非常に一般的な筋書きをコード化するからです。
この概念を型システムの観点で表現することは、コンパイラが、プログラマが処理すべき場面全てを処理していることをチェックできることを意味します;
この機能は、他の言語において、究極的にありふれたバグを阻止することができます。
プログラミング言語のデザインは、しばしばどの機能を入れるかという観点で考えられるが、 除いた機能も重要なのです。Rustには、他の多くの言語にはあるnull機能がありません。 nullとはそこに何も値がないことを意味する値です。nullのある言語において、 変数は常に二者択一どちらかの状態になります: nullかそうでないかです。
nullの開発者であるトニー・ホーア(Tony Hoare)の2009年のプレゼンテーション、 "Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)では、こんなことが語られています。
私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
null値の問題は、nullの値をnullでない値のように使用しようとしたら、何らかの種類のエラーが出ることです。 このnullかそうでないかという特性は広く存在するので、この種の間違いを大変犯しやすいのです。
しかしながら、nullが表現しようとしている概念は、それでも役に立つものです: nullは、 何らかの理由で現在無効、または存在しない値のことなのです。
問題は、全く概念にあるのではなく、特定の実装にあるのです。そんな感じなので、Rustにはnullがありませんが、
値が存在するか不在かという概念をコード化するenumならあります。このenumがOption<T>
で、
以下のように標準ライブラリに定義されています。
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Option<T>
は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。
さらに、列挙子もそうなっています: Some
とNone
をOption::
の接頭辞なしに直接使えるわけです。
ただ、Option<T>
はそうは言っても、普通のenumであり、Some(T)
とNone
もOption<T>
型のただの列挙子です。
<T>
という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、
第10章で解説します。とりあえず、知っておく必要があることは、<T>
は、Option
enumのSome
列挙子が、
あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、
Option
値を使って、数値型や文字列型を保持する例です。
#![allow(unused)] fn main() { let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; }
Some
ではなく、None
を使ったら、コンパイラにOption<T>
の型が何になるかを教えなければいけません。
というのも、None
値を見ただけでは、Some
列挙子が保持する型をコンパイラが推論できないからです。
Some
値がある時、値が存在するとわかり、その値は、Some
に保持されています。None
値がある場合、
ある意味、nullと同じことを意図します: 有効な値がないのです。では、なぜOption<T>
の方が、
nullよりも少しでも好ましいのでしょうか?
簡潔に述べると、Option<T>
とT
(ここでT
はどんな型でもいい)は異なる型なので、
コンパイラがOption<T>
値を確実に有効な値かのようには使用させてくれません。
例えば、このコードはi8
をOption<i8>
に足そうとしているので、コンパイルできません。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
このコードを動かしたら、以下のようなエラーメッセージが出ます。
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
(エラー: `i8: std::ops::Add<std::option::Option<i8>>`というトレイト境界が満たされていません)
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
なんて強烈な!実際に、このエラーメッセージは、i8
とOption<i8>
が異なる型なので、
足し合わせる方法がコンパイラにはわからないことを意味します。Rustにおいて、i8
のような型の値がある場合、
コンパイラが常に有効な値であることを確認してくれます。この値を使う前にnullであることをチェックする必要なく、
自信を持って先に進むことができるのです。Option<i8>
がある時(あるいはどんな型を扱おうとしていても)のみ、
値を保持していない可能性を心配する必要があるわけであり、
コンパイラはプログラマが値を使用する前にそのような場面を扱っているか確かめてくれます。
言い換えると、T
型の処理を行う前には、Option<T>
をT
に変換する必要があるわけです。一般的に、
これにより、nullの最もありふれた問題の一つを捕捉する一助になります: 実際にはnullなのに、
そうでないかのように想定することです。
不正確にnullでない値を想定する心配をしなくてもよいということは、コード内でより自信を持てることになります。
nullになる可能性のある値を保持するには、その値の型をOption<T>
にすることで明示的に同意しなければなりません。
それからその値を使用する際には、値がnullである場合を明示的に処理する必要があります。
値がOption<T>
以外の型であるところ全てにおいて、値がnullでないと安全に想定することができます。
これは、Rustにとって、意図的な設計上の決定であり、nullの普遍性を制限し、Rustコードの安全性を向上させます。
では、Option<T>
型の値がある時、その値を使えるようにするには、どのようにSome
列挙子からT
型の値を取り出せばいいのでしょうか?
Option<T>
には様々な場面で有効に活用できる非常に多くのメソッドが用意されています;
ドキュメントでそれらを確認できます。Option<T>
のメソッドに馴染むと、
Rustの旅が極めて有益になるでしょう。
一般的に、Option<T>
値を使うには、各列挙子を処理するコードが欲しくなります。
Some(T)
値がある時だけ走る何らかのコードが欲しくなり、このコードが内部のT
を使用できます。
None
値があった場合に走る別のコードが欲しくなり、そちらのコードはT
値は使用できない状態になります。
match
式が、enumとともに使用した時にこれだけの動作をする制御フロー文法要素になります:
enumの列挙子によって、違うコードが走り、そのコードがマッチした値の中のデータを使用できるのです。