Enumを定義する

コードで表現したくなるかもしれない場面に目を向けて、enumが有用でこの場合、構造体よりも適切である理由を確認しましょう。 IPアドレスを扱う必要が出たとしましょう。現在、IPアドレスの規格は二つあります: バージョン4とバージョン6です。 これらは、プログラムが遭遇するIPアドレスのすべての可能性です: 列挙型は、取りうる値をすべて列挙でき、 これが列挙型の名前の由来です。

どんなIPアドレスも、バージョン4かバージョン6のどちらかになりますが、同時に両方にはなり得ません。 IPアドレスのその特性により、enumデータ構造が適切なものになります。というのも、 enumの値は、その列挙子のいずれか一つにしかなり得ないからです。バージョン4とバージョン6のアドレスは、 どちらも根源的にはIPアドレスですから、コードがいかなる種類のIPアドレスにも適用される場面を扱う際には、 同じ型として扱われるべきです。

この概念をコードでは、IpAddrKind列挙型を定義し、IPアドレスがなりうる種類、V4V6を列挙することで、 表現できます。これらは、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::V4IpAddrKind::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"),
};
}

リスト6-1: IPアドレスのデータとIpAddrKindの列挙子をstructを使って保持する

ここでは、二つのフィールドを持つIpAddrという構造体を定義しています: IpAddrKind型(先ほど定義したenumですね)のkindフィールドと、 String型のaddressフィールドです。この構造体のインスタンスが2つあります。最初のインスタンス、 homeにはkindとしてIpAddrKind::V4があり、紐付けられたアドレスデータは127.0.0.1です。 2番目のインスタンス、loopbackには、kindの値として、IpAddrKindのもう一つの列挙子、V6があり、 アドレス::1が紐付いています。構造体を使ってkindaddress値を一緒に包んだので、 もう列挙子は値と紐付けられています。

各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、 同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddrの定義は、 V4V6列挙子両方に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),
}
}

リスト6-2: 列挙子各々が異なる型と量の値を格納するMessage enum

この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)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。 さらに、列挙子もそうなっています: SomeNoneOption::の接頭辞なしに直接使えるわけです。 ただ、Option<T>はそうは言っても、普通のenumであり、Some(T)NoneOption<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>値を確実に有効な値かのようには使用させてくれません。 例えば、このコードはi8Option<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>`
  |

なんて強烈な!実際に、このエラーメッセージは、i8Option<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の列挙子によって、違うコードが走り、そのコードがマッチした値の中のデータを使用できるのです。

関連キーワード:  定義, 列挙, Option, IpAddrKind, コード, let, データ, IpAddr, Some, allow