はじめに
列挙型の定義へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、IPアドレスの種類を表すために、IpAddrKind
と呼ばれる列挙型を定義します。これには、バージョン4 (V4
) とバージョン6 (V6
) が含まれます。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
列挙型の定義へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、IPアドレスの種類を表すために、IpAddrKind
と呼ばれる列挙型を定義します。これには、バージョン4 (V4
) とバージョン6 (V6
) が含まれます。
構造体は、関連するフィールドとデータをまとめる方法を提供します。たとえば、width
と height
を持つ Rectangle
のようにです。列挙型は、値が可能な値のセットの1つであることを表す方法を提供します。たとえば、Rectangle
は、Circle
や Triangle
も含む、可能な形状のセットの1つであると言いたい場合があります。これを行うには、Rustはこれらの可能性を列挙型としてエンコードすることを許可しています。
コードで表現したい状況を見てみましょう。列挙型が役に立つ理由と、この場合に構造体よりも適切な理由を理解しましょう。IPアドレスを扱う必要があるとしましょう。現在、IPアドレスには2つの主要な標準が使用されています。バージョン4とバージョン6です。これらは、プログラムが遭遇するIPアドレスの唯一の可能性であるため、すべての可能なバリアントを 列挙 することができます。ここから列挙型の名前が来ています。
任意のIPアドレスは、バージョン4またはバージョン6のアドレスのいずれかであり、同時に両方ではありません。IPアドレスのこの特性は、列挙型データ構造を適切にするため、列挙型の値はそのバリアントの1つだけであることができます。バージョン4とバージョン6の両方のアドレスは、基本的にはIPアドレスです。したがって、コードが任意の種類のIPアドレスに適用される状況を処理する場合、同じ型として扱う必要があります。
この概念をコードで表現するには、IpAddrKind
列挙型を定義して、IPアドレスがあり得る可能性のある種類である V4
と V6
をリストします。これらは列挙型のバリアントです。
enum IpAddrKind {
V4,
V6,
}
IpAddrKind
は、コードの他の場所で使用できるカスタムデータ型になりました。
IpAddrKind
の2つのバリアントのそれぞれのインスタンスをこのように作成できます。
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
列挙型のバリアントは、その識別子の下に名前空間があり、2つを区切るためにダブルコロンを使用します。これは便利です。なぜなら、IpAddrKind::V4
と IpAddrKind::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
です。構造体を使って kind
と address
の値をまとめています。したがって、バリアントが値に関連付けられています。
ただし、列挙型だけを使って同じ概念を表現する方が簡潔です。構造体の中の列挙型ではなく、列挙型の各バリアントに直接データを入れることができます。IpAddr
列挙型のこの新しい定義は、V4
と V6
の両方のバリアントに関連付けられた 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::
接頭辞なしで直接 Some
と None
を使用できます。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>
と T
(T
は任意の型)は異なる型であるため、コンパイラは Option<T>
値を必ずしも有効な値として使用しようとしないようにします。たとえば、このコードはコンパイルされません。なぜなら、i8
に Option<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が i8
と Option<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でさらに実験を行って、スキルを向上させることができます。