🌱

【Rust】Enumとmatchがよく分からん。という人に向けて ~Optionを添えて~

2024/03/14に公開
5

はじめに

本記事はrust book 6章を元に、もう少し詳しくに解説しています。

コードサンプルや解説をもっと知りたいという方を対象としています。

Enum

RustでEnumは以下のように定義できます。

enum IpAddrKind {
    V4,
    V6,
}

そしてそれぞれを出力すると、V4,V6と表示されます。

#[derive(Debug)]
enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    println!("{:?}", IpAddrKind::V4); // V4
    println!("{:?}", IpAddrKind::V6); // V6
}

Enumは個別に型を持てる

Enumはそれぞれのvariantに値を持つことができます。
そのため、Enumを包含した余計な型を作る手間を省くことができます。

#[derive(Debug)]
enum IpAddrKind {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    println!("{:?}", IpAddrKind::V4(127, 0, 0, 1)); // V4(127, 0, 0, 1)
    println!("{:?}", IpAddrKind::V6(String::from("::1"))); // V6("::1")
}

メソッドを定義できる

enumは構造体と同じようにメソッドを実装できます。
先ほどの例のprintln!を共通化してみましょう。

call()メソッドを定義しました。

#[derive(Debug)]
enum IpAddrKind {
    V4(u8, u8, u8, u8),
    V6(String),
}

// メソッドを定義
impl IpAddrKind {
    fn call(&self) {
        println!("{:?}", self);
    }
}

fn main() {
    IpAddrKind::V4(127, 0, 0, 1).call(); // V4(127, 0, 0, 1)
    IpAddrKind::V6(String::from("::1")).call(); // V6("::1")
}

match

Rustにはパターンマッチングのためのmatchと呼ばれる制御フロー構文があります。Goのswitch文と似ています。

enum Color {
    Red,
    Blue,
    Green,
}

fn main() {
    let color = Color::Red;
    let color_jp = color_to_jp(color);

    println!("{}", color_jp); // あか
}

fn color_to_jp(color: Color) -> String {
    match color {
        Color::Red => String::from("あか"),
        Color::Blue => String::from("あお"),
        Color::Green => String::from("みどり"),
    }
}

Enumに個別の型がある場合

Enumには個別に値を持てると前述しましたが、その場合はmatchの中で()を書くことで引数のように受け取れます。

値が不要な場合はColor::Red(_)Color::Red(..)と記述します。

赤だけにカラーコードを持たせてみましょう。そして、matchした場合はカラーコードをprintするようにコードを書き直します。

#[allow(dead_code)]
enum Color {
    Red(String),
    Blue,
    Green,
}

fn main() {
    let color = Color::Red(String::from("#ff0000"));
    let color_jp = color_to_jp(color);

    println!("{}", color_jp); // あか
}

fn color_to_jp(color: Color) -> String {
    match color {
        Color::Red(color_code) => {
            println!("{}", color_code); // #ff0000
            String::from("あか")
        }
        Color::Blue => String::from("あお"),
        Color::Green => String::from("みどり"),
    }
}

matchは全てのケースを書くこと

matchでは全てのケースを書かないとエラーとなります。そこで、個別に書かなくていい場合は_(アンダースコア)を使用して、その他をまとめて表現できます。

#[allow(dead_code)]
enum Color {
    Red,
    Blue,
    Green,
}

fn main() {
    let color = Color::Blue;
    let color_jp = color_to_jp(color);

    println!("{}", color_jp); // その他
}

fn color_to_jp(color: Color) -> String {
    match color {
        Color::Red => String::from("あか"),
        _ => String::from("その他"),
    }
}

Enum以外にも利用できる

ここまでEnumのmatchについて説明しましたが、Enum以外にも使用することができます。

fn main() {
    let number = 2;

    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        _ => println!("Other"),
    }
}

Optionを使いこなそう

ご存知の通り、RustにはNullがありません。その代わりにこのOptionを使います。
そして中身はシンプルです。2つのバリアンとしかありません。

Nullになる可能性がある値は、Optionでラップした型を使います。
値がある場合はSome, 値がない場合はNoneを設定します。

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

使い方

値がある場合はSome(値)としてSomeの中に値を格納します。
値が無い場合はNoneとして無いことを表現します。

matchを使って、SomeNoneのEnumでパターンマッチングを行います。

fn main() {
    let some_value = Some(String::from("Hello")); // Someは値がある
    let none_value: Option<String> = None; // Noneは値が無い

    println!("{}", response(some_value)); // Hello World
    println!("{}", response(none_value)); // No value
}

fn response(value: Option<String>) -> String {
    match value {
        Some(value) => {
            value + " World"
        }
        None => String::from("No value"),
    }
}

Discussion

kanaruskanarus

マサカリ

本題

Enumはそれぞれのフィールドに型を持つことができます。

Enumには個別に型を持てる

  • Enum の variant を「フィールド」とは普通言わないと思います
  • 「 ( Enum がそれぞれの variant に ) 型を持つ」という表現は変な気がします
    • 「値を持つ」や「フィールドを持つ」なら問題なさそうです

「 Enumはそれぞれのvariantに値を持つことができます。」あたりがここの適当な表現ではないでしょうか (?)


Enumには個別に型を持てると前述しましたが、型を持っている場合はmatchの中で()を書くことで...

対して構造体は関連するデータをグループ化するために使用されます。フィールドに名前を付けることで、...

はおそらく誤解を含んでいて、enum の variant も名前付きフィールドを持てます (

enum Color {
    Red { color_code: String },
    Blue,
    Green,
}

) し、struct が必ずしも名前のあるフィールドを持っているとは限りません (

struct Point2D(f64, f64);

struct Resolver;

) 。また、

持っている場合はmatchの中で()を書くことで引数のように受け取れます

() を書いても書かなくてもいいように読めますが、

enum Color {
    Red(String),
    // ...
}

Red にマッチさせる場合、対応する Color::Red( /* 〜 */ ) というパターンを書く必要があります ( 中の値がいらなければ Color::Red(_)Color::Red(..) とします ) 。


その他

Rustにはパターンマッチングのためのmatchと呼ばれる制御フロー演算子があります。

match は演算子ではないと思います。「制御フロー構文」ですね

matchでは全てのケースを書かないとエラーとなります。そこで、個別に書かなくていい場合は_(アンダースコア)を使用して、その他をまとめて表現できます。

必ずしも _ で捨てる必要はなくて、

match color {
    Color::Red => String::from("あか"),
    other => /* `other` を使ったなにか */,
}

とすることもできます。

ここまでEnumのmatchについて説明しましたが、Enum以外にも使用することができます。
例としてnumber型で書いてみましょう。

これは typo に近そうですが、number 型ではないです ( 一応 )

Nullになる可能性があるフィールドは、Optionでラップた型を使います。
値がある場合はSome, 値がない場合(Null)はNoneを設定します。

は誤解を招く書き方だと思います。一般に Option::None はいわゆる null とは違うものなので、

「値がない可能性があるときは、Optionでラップした型を使います。
値がある場合はSome(値), 値がない場合はNoneで表現します。」

あたりが適当な説明ではないでしょうか (?)

補足

Rust の enum はタグ付き union で、SomeNone 自体は単なるタグです。( その上でコンパイラによる最適化の一環として None が null と同一視される場合もありますが ) プログラマ側のマインドセットとしては「使い方」の項の

値がある場合はSome(値)としてSomeの中に値を格納します。
値が無い場合はNoneとして無いことを表現します。

が全てで、そもそも null という概念を意識する必要がないです。


お願い

初学者向けの記事とはいえ事実と違う点は指摘するべきかなと思ってマサカリを投げてみましたが、Rust に興味を持って記事を書く人がいるのは純粋に嬉しいので、どうかこんなことで萎縮せず Rust に関わり続けてほしいです... !

TotsukaTotsuka

ありがとうございます!

自分が知らないこともあり、とても助かります。
一点、制御フロー演算子Rust book 6.2に記述してあり、ここに合わせて書いた経緯があります。

構文の方が自然でしょうか?