ユーニックス総合研究所

  • home
  • archives
  • rust-enum

Rustのenumの使い方【match文、エラーハンドリング】

  • 作成日: 2024-02-16
  • 更新日: 2024-02-16
  • カテゴリ: Rust

Rustで列挙型を定義したい場合、Rustではenumが使えます。
Rustのenumは他言語のenumに比べると非常にパワフルで表現が多彩です。
そのためエラーハンドリングでも便利に使うことができます。

Rustのenumの性質を理解して使いこなせば柔軟なコードを書くことが出来るようになるでしょう。
この記事ではRustのenumについて解説します。

Enumを定義する - The Rust Programming Language 日本語版

enumの定義方法

Rustではenumは以下のように定義します。

enum 列挙型名 {  
    列挙子,  
    列挙子,  
    列挙子,  
    ...  
}  

ここでは簡単にAnimalという列挙型名で列挙子Cat, Dogを定義してみます。

enum Animal {  
    Cat,  
    Dog,  
}  

enumの各列挙子はカンマで区切ります。
このように複数の列挙子を列挙できるのがenumの特徴です。

enum列挙型名も列挙子も命名規則は全ての単語の先頭文字を大文字にするパスカルケースです。

enum MyAnimal {  
    Cat,  
    WaterDog,  
    GreenPullDog,  
}  

列挙子は複数の同じ名前で列挙することはできません。

enum Animal {  
    Cat,  
    Cat,  
}  

// error[E0428]: the name `Cat` is defined multiple times  

上記のような定義はエラーになります。

enumのインスタンスの生成

enumの列挙子はインスタンスとして変数にすることができます。
以下のようなコードを書きます。

    let cat = Animal::Cat;  
    let dog = Animal::Dog;  

enumAnimalの列挙子を参照するには上記のようにコロンを2つ付けて参照します。

enumのifでの参照

RustのenumのインスタンスはC/C++などと違ってifで参照することはできません。
たとえば以下のようなコードはエラーになります。

    if cat == Animal::Cat {  
    }  

    // error[E0369]: binary operation `==` cannot be applied to type `Animal`  

変数catAnimal::Catのインスタンスなので直感的にはifでも上記のように書けそうに見えます。
しかしRustのバイナリオペレーション==はそのような式には対応していません。
ではどうやって条件分岐したらいいのか? というところですが、こういう場合はmatchを使います。

enumのmatchでの参照

enumのインスタンスはmatchで参照できます。

    let cat = Animal::Cat;  
    let dog = Animal::Dog;  

    match cat {  
    Animal::Cat => {  
        println!("Cat!");  
    }  
    _ => {}  
    }  
    // Cat!  

    match dog {  
    Animal::Dog => {  
        println!("Dog!");  
    }  
    _ => {}  
    }  
    // Dog!  

上記のようにmatchの分岐でAnimal::Catなどの列挙子を指定します。
そうすると変数がその列挙子にマッチしたときにその分岐に移行します。

matchの実用的な使い方としては何の列挙子が返ってくるかわからない関数の返り値を処理する時などが例として挙げられます。

// 引数sに応じてenumのAnimalを返す  
fn judge(s: &str) -> Animal {  
    match s {  
        "cat" => Animal::Cat,  
        "dog" => Animal::Dog,  
        &_ => Animal::Cat,  
    }  
}  

fn test() {  
    // Animalを返すjudge()の呼び出し  
    let var = judge("cat");  

    // 何の列挙子が返ってきたか調べる  
    match var {  
    Animal::Cat => {  
        println!("Cat!");  
    }  
    Animal::Dog => {  
        println!("Dog!");  
    }  
    }  
}  

上記の場合はjudge()関数は引数の文字列リテラルに応じてAnimal列挙子を返します。
judge()の呼び出し側ではjudge()の返り値をmatchでマッチして、出力します。

enumにデータを持たせる

ここからRustのenumのパワフルなところを見ていきます。
Rustのenumにはデータを持たせることができます。

C/C++などではenumと合わせてデータを持たせたい場合はenumを構造体でラップして個別に変数を定義したりします。
しかしRustのenumの場合はenumだけで完結できます。

たとえば列挙子にStringを持たせたい場合は以下のように定義します。

// ロボットを表すenum  
enum Robot {  
    MyRobo(String),  
    OtherRobo(String),  
}  

この定義では列挙子を変数にするときに文字列を変数に格納することができます。

    let myrobo = Robot::MyRobo(String::from("myrobo"));  
    let otherrobo = Robot::OtherRobo(String::from("otherrobo"));  

このenumの大事な点はenumの列挙子としての性格はそのままだということです。
つまりmatchなどでは列挙子でマッチさせることができます。

変数に文字列を入れているんだからifなどで文字列と比較できるのか?
というところですが、これはできません。

    if myrobo == "myrobo" {  
    }  

    // error[E0369]: binary operation `==` cannot be applied to type `Robot`  

変数myroboはあくまで列挙子ですので、文字列ではないからです。
またmatchで文字列とマッチさせることもできません。

    match myrobo {  
    "myrobo" => {  
        println!("MyRobo!");  
    }  
    }  

    // error[E0308]: mismatched types. expected `Robot`, found `&str`  

myroboは列挙子です。
そのためmatchでマッチさせる場合は以下のようにコードを書きます。

    match myrobo {  
    Robot::MyRobo(inner_string) => {  
        println!("{inner_string}");  
    }  
    _ => {}  
    }  
    // myrobo  

    match otherrobo {  
    Robot::OtherRobo(inner_string) => {  
        println!("{inner_string}");  
    }  
    _ => {}  
    }  
    // otherrobo  

inner_stringというのが列挙子が持っている文字列のデータです。
matchで参照するときにこのようにデータの識別子を書いておくことで参照できるようになります。

enumに複数のデータを持たせる

Rustのenumには複数のデータを持たせることができます。
先ほどはString1つだけでしたが以下のように複数の型を指定できます。

// クラスの年組を表すenum  
enum Classroom {  
    Sakura(usize, usize),  
    Ume(usize, usize),  
}  

上記の例ではusizeだけにしていますが、もちろんusizeの他の型も指定できます。
また複数の型を混合して定義することもできます。

列挙子をインスタンスにするときにデータを渡して紐づけます。
これはStringの場合と一緒です。

    let sakura = Classroom::Sakura(3, 2);  // 桜 3年2組  
    let ume = Classroom::Ume(2, 7);  // 梅 2年7組  

    match sakura {  
    Classroom::Sakura(nen, kumi) => {  
        println!("桜 {nen}年{kumi}組");  
    }  
    _ => {}  
    }  
    // 桜 3年2組  

    match ume {  
    Classroom::Ume(nen, kumi) => {  
        println!("梅 {nen}年{kumi}組");  
    }  
    _ => {}  
    }  
    // 梅 2年7組  

列挙子としての表現の他にデータを紐づけられるので、Rustのenumはかなり表現力が高いと言えます。

enumを使ったエラーハンドリング

Rustではエラーハンドリングをするときにenumを使うことができます。
enumにはデータをバインドできますので、これによって多彩なエラー表現が可能になります。

// enumでErrorを定義  
enum Error {  
    InvalidArg(String),  // エラー:不正な引数  
}  

fn func(n: i32) -> Result<i32, Error> {  
    if n < 0 {  
        // nが0より下ならエラーにする  
        return Err(Error::InvalidArg(format!("{n} is under 0")));  
    }  

    Ok(n * 2)  
}  

fn test() {  
    // エラーが起こる可能性のある関数を呼び出し  
    let result = func(-1);  

    // エラーハンドリング  
    match result {  
    Err(err) => {  
        // 列挙子からデータ(エラーメッセージ)を取り出す  
        match err {  
        Error::InvalidArg(s) => {  
            println!("Error! {}", s);  
        }  
        }  
    }  
    Ok(n) => {  
        println!("{n}");  
    }  
    }  
}  

まずエラーを表すenumですが以下のように定義します。

// enumでErrorを定義  
enum Error {  
    InvalidArg(String),  // エラー:不正な引数  
}  

列挙子InvalidArgStringを持てます。
次にこのErrorを使った関数を定義します。

fn func(n: i32) -> Result<i32, Error> {  
    if n < 0 {  
        // nが0より下ならエラーにする  
        return Err(Error::InvalidArg(format!("{n} is under 0")));  
    }  

    Ok(n * 2)  
}  

Result<i32, Error>は成功時にi32を、エラー時にErrorを返す返り値の型です。
関数の引数n0より下ならErr()でエラーを返します。

Err(Error::InvalidArg(format!("{n} is under 0")));      

Error::InvalidArgにはformat!()で整形した文字列を渡しています。
こうすることで引数nの値がエラーハンドリング時にわかるようになります。
また、Errorの列挙子にnを格納するデータを持たせてもいいでしょう。
この辺は設計者の思想によって変わるかと思います。

    // エラーが起こる可能性のある関数を呼び出し  
    let result = func(-1);  

    // エラーハンドリング  
    match result {  
    Err(err) => {  
        // 列挙子からデータ(エラーメッセージ)を取り出す  
        match err {  
        Error::InvalidArg(s) => {  
            println!("Error! {}", s);  
        }  
        }  
    }  
    Ok(n) => {  
        println!("{n}");  
    }  
    }  

Resultを返す関数の返り値はmatchでエラーハンドリングできます。
上記ではまずresultmatchで分岐して成功時にはnの値を出力し、エラー時にはエラーメッセージを出力します。
Err(err)で取り出しているerrは列挙子のインスタンスですのでこの段階では文字列はまだ取り出していません。
エラー文字列を取り出したい場合はさらにmatchを入れ子にしてerrからsを取り出します。
この表現は実質、他言語の例外処理に近しいかまたは同等になると言えるでしょう。

enumを文字列にする

enumのインスタンスを文字列にしたい場合はstd::fmt::Displayを実装してto_string()メソッドを実装します。

impl std::fmt::Display for Error {  
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {  
        match self {  
            Error::InvalidArg(msg) => write!(f, "Invalid argument: {}", msg),  
        }  
    }  
}  

fn test() {  
    let err = Error::InvalidArg(String::from("error!"));  
    println!("{}", err.to_string());  // Invalid argument: error!  
}