Enumを定義する

構造体は、widthheightを持つRectangleのように、関連するフィールドとデータをひとつにまとめる方法を提供してくれます。 一方でenumは、ある値が、とりうる値の集合のうちのいずれかひとつであることを表現する方法を提供するものです。 例えば、Rectangleは、CircleTriangleも含めたとりうる形の集合のいずれかひとつである、と表現したいことがあります。 これを達成するために、Rustではこれらの可能性をenumとしてエンコードすることができます。

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

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

この概念をコードでは、IpAddrKind列挙型を定義し、IPアドレスがなりうる種類、V4V6を列挙することで、 表現できます。これらが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の各列挙子のインスタンスは生成できます:

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::V4IpAddrKind::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"),
    };
}

リスト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値が紐付けられていることを述べています。

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がどう機能するかについての別の詳細も確認することができます: ここで定義したenumの各列挙子の名前は、そのenumを構築する関数にもなるのです。 つまり、IpAddr::V4()String引数を受け取りIpAddr型のインスタンスを返す関数の呼び出しになるのです。 enumを定義した結果として、自動でこのコンストラクタ関数が定義されます。

構造体よりもenumを使うことには、別の利点もあります: 各列挙子に紐付けるデータの型と量は、異なってもいいのです。 バージョン4のIPアドレスには、常に0から255の値を持つ4つの数値があります。V4のアドレスは、4つのu8型の値として格納するけれども、 V6のアドレスは引き続き、単独の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と列挙子がありますが、アドレスデータを二種の異なる構造体の形で列挙子に埋め込み、 この構造体は各列挙子用に異なる形で定義されています。

#![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() {}

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

このenumには、異なる型の列挙子が4つあります:

  • Quitは関連付けられたデータをまったく持ちません。
  • Moveは構造体のように名前付きのフィールドを持っています。
  • WriteStringオブジェクトを1個だけ含んでいます。
  • ChangeColorは3個のi32値を含んでいます。

リスト6-2のような列挙子を含むenumを定義することは、enumの場合、structキーワードを使わず、 全部の列挙子がMessage型の元に分類される点を除いて、異なる種類の構造体定義を定義するのと類似しています。 以下の構造体も、先ほどのenumの列挙子が保持しているのと同じデータを格納することができるでしょう:

struct QuitMessage; // ユニット構造体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // タプル構造体
struct ChangeColorMessage(i32, i32, i32); // タプル構造体

fn main() {}

ですがそれぞれ独自の型を持つ異なる構造体を使ってしまうと、リスト6-2で定義した単一の型であるMessage enumを使う場合と比較して、 これらの任意の種類のメッセージを取る関数を簡単には定義できません。

enumと構造体にはもう1点似通っているところがあります: implを使って構造体にメソッドを定義できるのと全く同様に、 enumにもメソッドを定義することができるのです。こちらは、Message enum上に定義できるcallという名前のメソッドです:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // メソッド本体はここで定義されます
        }
    }

    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値に勝る利点

この節では、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> {
    None,
    Some(T),
}
}

Option<T>は有益すぎて、preludeにさえ含まれています; 明示的にスコープに導入する必要がないのです。 そしてその列挙子もまたpreludeに含まれています: SomeNoneOption::の接頭辞なしに直接使えるわけです。 ただ、Option<T> enumはそうは言っても、普通のenumであり、Some(T)NoneOption<T>型のただの列挙子です。

<T>という記法は、まだ語っていないRustの機能です。 これはジェネリック型引数で、ジェネリクスについて詳しくは第10章で解説します。 とりあえず<T>の意味に関して知っておく必要があることは、Option enumのSome列挙子はあらゆる型のデータを1つだけ持つことができ、 Tの代わりに使用される具体的な型に応じて、Option<T>型はそれぞれ異なる型になるということだけです。 以下はOption値を使って数値型や文字列型を保持する例です。

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>で、これは先ほどとは異なる型です。 Some列挙子の中で値を指定しているので、コンパイラはこれらの型を推論することができます。 absent_numberに関しては、コンパイラはジェネリクス適用後のOption型を注釈することを要求します: None値を見ただけでは、それに対応するSome列挙子が保持する型をコンパイラは推論できないからです。 ここでは、absent_numberOption<i32>型を持つことをコンパイラに伝えています。

Some値がある時、値が存在するとわかり、その値は、Someに保持されています。None値がある場合、 ある意味、nullと同じことを意図します: 有効な値がないのです。では、なぜOption<T>の方が、 nullよりも少しでも好ましいのでしょうか?

簡潔に述べると、Option<T>T(ここでTはどんな型でもいい)は異なる型なので、 コンパイラがOption<T>値を確実に有効な値かのようには使用させてくれません。 例えば、このコードはi8Option<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`
(エラー: `Option<i8>`を`i8`に足すことはできません)
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
                      (`i8 + `Option<i8>`のための実装がありません)
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`
    (ヘルプ: トレイト`Add<Option<i8>`が`i8`に対して実装されていません)
  = help: the following other types implement trait `Add<Rhs>`:
    (ヘルプ: 以下の型であればトレイト`Add<Rhs>`を実装しています:)
            <i8 as Add>
            <i8 as Add<&i8>>
            <&'a i8 as Add<i8>>
            <&i8 as Add<&i8>>

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

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