列挙型の定義

Beginner

This tutorial is from open-source community. Access the source code

はじめに

列挙型の定義へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、IP アドレスの種類を表すために、IpAddrKindと呼ばれる列挙型を定義します。これには、バージョン 4 (V4) とバージョン 6 (V6) が含まれます。

列挙型の定義

構造体は、関連するフィールドとデータをまとめる方法を提供します。たとえば、widthheight を持つ Rectangle のようにです。列挙型は、値が可能な値のセットの 1 つであることを表す方法を提供します。たとえば、Rectangle は、CircleTriangle も含む、可能な形状のセットの 1 つであると言いたい場合があります。これを行うには、Rust はこれらの可能性を列挙型としてエンコードすることを許可しています。

コードで表現したい状況を見てみましょう。列挙型が役に立つ理由と、この場合に構造体よりも適切な理由を理解しましょう。IP アドレスを扱う必要があるとしましょう。現在、IP アドレスには 2 つの主要な標準が使用されています。バージョン 4 とバージョン 6 です。これらは、プログラムが遭遇する IP アドレスの唯一の可能性であるため、すべての可能なバリアントを 列挙 することができます。ここから列挙型の名前が来ています。

任意の IP アドレスは、バージョン 4 またはバージョン 6 のアドレスのいずれかであり、同時に両方ではありません。IP アドレスのこの特性は、列挙型データ構造を適切にするため、列挙型の値はそのバリアントの 1 つだけであることができます。バージョン 4 とバージョン 6 の両方のアドレスは、基本的には IP アドレスです。したがって、コードが任意の種類の IP アドレスに適用される状況を処理する場合、同じ型として扱う必要があります。

この概念をコードで表現するには、IpAddrKind 列挙型を定義して、IP アドレスがあり得る可能性のある種類である V4V6 をリストします。これらは列挙型のバリアントです。

enum IpAddrKind {
    V4,
    V6,
}

IpAddrKind は、コードの他の場所で使用できるカスタムデータ型になりました。

列挙型の値

IpAddrKind の 2 つのバリアントのそれぞれのインスタンスをこのように作成できます。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

列挙型のバリアントは、その識別子の下に名前空間があり、2 つを区切るためにダブルコロンを使用します。これは便利です。なぜなら、IpAddrKind::V4IpAddrKind::V6 の両方の値が同じ型 IpAddrKind であるからです。そのため、たとえば、任意の IpAddrKind を受け取る関数を定義できます。

fn route(ip_kind: IpAddrKind) {}

そして、どちらのバリアントでもこの関数を呼び出すことができます。

route(IpAddrKind::V4);
route(IpAddrKind::V6);

列挙型を使用するとさらに利点があります。IP アドレスの型についてもう少し考えてみましょう。現時点では、実際の IP アドレス データ を格納する方法がありません。どの 種類 であるかだけを知っています。第 5 章で構造体について学んだばかりなので、構造体を使ってこの問題を解決しようとするかもしれません。リスト 6-1 に示すようにです。

1 enum IpAddrKind {
    V4,
    V6,
}

2 struct IpAddr {
  3 kind: IpAddrKind,
  4 address: String,
}

5 let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

6 let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

リスト 6-1:struct を使って IP アドレスのデータと IpAddrKind バリアントを格納する

ここでは、2 つのフィールドを持つ IpAddr 構造体を定義しています [2]。1 つは kind フィールドで [3]、その型は IpAddrKind です(先に定義した列挙型 [1])。もう 1 つは address フィールドで [4]、その型は String です。この構造体のインスタンスは 2 つあります。最初は home で [5]、kind には IpAddrKind::V4 の値があり、関連付けられたアドレスデータは 127.0.0.1 です。2 番目のインスタンスは loopback で [6]。kind の値には IpAddrKind のもう 1 つのバリアントである V6 があり、関連付けられたアドレスは ::1 です。構造体を使って kindaddress の値をまとめています。したがって、バリアントが値に関連付けられています。

ただし、列挙型だけを使って同じ概念を表現する方が簡潔です。構造体の中の列挙型ではなく、列挙型の各バリアントに直接データを入れることができます。IpAddr 列挙型のこの新しい定義は、V4V6 の両方のバリアントに関連付けられた String 型の値があることを意味します。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

列挙型の各バリアントに直接データを付け加えるので、追加の構造体は不要です。ここでは、列挙型がどのように機能するかの別の詳細もわかりやすくなります。定義した各列挙型バリアントの名前も、列挙型のインスタンスを構築する関数になります。つまり、IpAddr::V4() は、String 型の引数を取り、IpAddr 型のインスタンスを返す関数呼び出しです。列挙型を定義することで、自動的にこのコンストラクタ関数が定義されます。

構造体よりも列挙型を使う利点がもう 1 つあります。各バリアントには、異なる型と量の関連付けられたデータを持つことができます。バージョン 4 の IP アドレスは常に 4 つの数値コンポーネントを持ち、その値は 0 から 255 の間になります。V4 アドレスを 4 つの u8 型の値として格納したいが、V6 アドレスを 1 つの String 型の値として表現したい場合、構造体ではこれを行うことができません。列挙型はこの場合を簡単に処理します。

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 をどのように定義しているか見てみましょう。これは、私たちが定義して使用した列挙型とバリアントがまったく同じですが、アドレスデータを 2 つの異なる構造体の形式でバリアントの中に埋め込んでいます。各バリアントで異なる定義がされています。

struct Ipv4Addr {
    --snip--
}

struct Ipv6Addr {
    --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

このコードは、列挙型のバリアントの中に任意の種類のデータを入れることができることを示しています。たとえば、文字列、数値型、または構造体です。さらに、別の列挙型も含めることができます!また、標準ライブラリの型は、自分で考えつくものよりも複雑ではありません。

標準ライブラリには IpAddr の定義が含まれていますが、まだ自分たちの定義を作成して使用することができます。なぜなら、標準ライブラリの定義をスコープに持ち込んでいないからです。第 7 章で型をスコープに持ち込む方法についてもっと詳しく説明します。

リスト 6-2 の列挙型の別の例を見てみましょう。これは、バリアントにさまざまな型が埋め込まれています。

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

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

この列挙型は、異なる型の 4 つのバリアントを持っています。

  • Quit にはまったく関連付けられたデータがありません。
  • Move には、構造体と同じように名前付きのフィールドがあります。
  • Write には、1 つの String が含まれています。
  • ChangeColor には、3 つの i32 型の値が含まれています。

リスト 6-2 のようなバリアントを持つ列挙型を定義することは、さまざまな種類の構造体定義を定義することに似ています。ただし、列挙型は struct キーワードを使用せず、すべてのバリアントが Message 型の下にグループ化されています。次の構造体は、前述の列挙型バリアントが保持するデータと同じデータを保持することができます。

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

しかし、それぞれ独自の型を持つ異なる構造体を使用した場合、リスト 6-2 で定義した Message 列挙型のように、これらの種類のメッセージのいずれかを受け取る関数を定義することはできません。列挙型と構造体にはもう 1 つの類似点があります。構造体では impl を使ってメソッドを定義できるように、列挙型でもメソッドを定義できます。ここでは、Message 列挙型に定義できる call という名前のメソッドを示します。

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

2 let m = Message::Write(String::from("hello"));
m.call();

メソッドの本体は、self を使ってメソッドを呼び出した値を取得します。この例では、Message::Write(String::from("hello")) の値を持つ変数 m を作成しています [2]。そして、m.call() が実行されるとき、call メソッドの本体 [1] で self はその値になります。

標準ライブラリにある非常に一般的で便利な別の列挙型を見てみましょう。Option です。

Option 列挙型と null 値に比べた利点

このセクションでは、標準ライブラリによって定義されるもう 1 つの列挙型である Option のケーススタディを探ります。Option 型は、値が何かあるか、または何もないという非常に一般的なシナリオをエンコードします。

たとえば、複数の要素を含むリストの最初の要素を要求すると、値が返されます。空のリストの最初の要素を要求すると、何も返されません。型システムの観点からこの概念を表現すると、コンパイラがすべき場合すべてを処理したかどうかをチェックできます。この機能により、他のプログラミング言語で非常に一般的なバグを防ぐことができます。

プログラミング言語の設計は、含める機能について考えられることが多いですが、除外する機能も同じく重要です。Rust には、他の多くの言語にある null 機能はありません。null は、そこに値がないことを意味する値です。null がある言語では、変数は常に 2 つの状態のいずれかになります。null または not-null です。

null の発明者であるトニー・ホアは、2009 年の講演「Null References: The Billion Dollar Mistake」でこう述べています。

これを私の 10 億ドルのミスと呼んでいます。当時、私はオブジェクト指向言語における参照用の最初の包括的な型システムを設計していました。私の目標は、すべての参照の使用が絶対的に安全であり、コンパイラによって自動的にチェックされることを確実にすることでした。しかし、実装が簡単だったため、私は null 参照を入れるという誘惑に負けました。これは、過去 40 年間でおそらく 10 億ドルの損害と苦しみをもたらした、数え切れないエラー、脆弱性、システムクラッシュにつながりました。null 値の問題は、null 値を not-null 値として使用しようとすると、何らかのエラーが発生するということです。この null または not-null の特性があまりにも普及しているため、この種のエラーを犯すのは非常に簡単です。

しかし、null が表現しようとしている概念は依然として有用です。null は、現在、何らかの理由で無効または存在しない値です。

問題は、本質的には概念ではなく、特定の実装にあります。そのため、Rust には null はありませんが、値が存在するか存在しないかという概念をエンコードできる列挙型があります。この列挙型は Option<T> で、標準ライブラリによって次のように定義されています。

enum Option<T> {
    None,
    Some(T),
}

Option<T> 列挙型は非常に便利で、プレリュードにも含まれています。明示的にスコープに持ち込む必要はありません。そのバリアントもプレリュードに含まれています。Option:: 接頭辞なしで直接 SomeNone を使用できます。Option<T> 列挙型は依然として通常の列挙型であり、Some(T)None は依然として Option<T> 型のバリアントです。

<T> の構文は、まだ話していない Rust の機能です。これはジェネリック型パラメータで、第 10 章でジェネリクスについてもっと詳しく説明します。今のところ、知っておく必要のあることは、<T>Option 列挙型の Some バリアントが任意の型の 1 つのデータを保持できることを意味し、T の代わりに使用される各具体的な型が全体の Option<T> 型を異なる型にすることです。次は、数値型と文字列型を保持するために Option 値を使用する例です。

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 バリアントが保持する型を推論することはできません。ここでは、absent_number の型が Option<i32> であることを Rust に伝えています。

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

簡単に言えば、Option<T>TT は任意の型)は異なる型であるため、コンパイラは Option<T> 値を必ずしも有効な値として使用しようとしないようにします。たとえば、このコードはコンパイルされません。なぜなら、i8Option<i8> を加えようとしているからです。

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

このコードを実行すると、次のようなエラーメッセージが表示されます。

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`

深刻です!実際、このエラーメッセージは、Rust が i8Option<i8> をどのように加えるかを理解できないことを意味しています。なぜなら、それらは異なる型だからです。Rust で i8 のような型の値がある場合、コンパイラは常に有効な値があることを保証します。その値を使用する前に null をチェックする必要はありません。Option<i8>(または作業している値の型)がある場合にのみ、値がない可能性があることを心配する必要があり、コンパイラは値を使用する前にその場合を処理するようにします。

言い換えると、Option<T>T に変換しなければ、それを使って T の操作を行うことはできません。一般的に、これは null に関する最も一般的な問題の 1 つをキャッチするのに役立ちます。実際は null であるのに、null でないと仮定することです。

not-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 式は、列挙型とともに使用するとこのような制御フロー構造を行います。列挙型のどのバリアントであるかに応じて異なるコードを実行し、そのコードは一致する値の中のデータを使用できます。

まとめ

おめでとうございます!列挙型の定義の実験を完了しました。LabEx でさらに実験を行って、スキルを向上させることができます。