Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

if letlet...else による簡潔な制御フロー

if let 構文を使うと、iflet を組み合わせて、1つのパターンに一致する値を処理し、それ以外を無視する方法をより簡潔に書けます。リスト6-6のプログラムでは、config_max 変数内の Option<u8> 値に対してマッチしていますが、値が Some バリアントの場合にだけコードを実行したいと考えています。

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}

値が Some であれば、パターン中で値を変数 max に束縛することにより、Some バリアント内の値を出力します。None の値に対しては何もしたくありません。match 式の要件を満たすために、1つのバリアントを処理したあとに _ => () を追加しなければならず、これは追加するのが面倒なボイラープレートコードです。

代わりに、if let を使ってもっと短く書けます。次のコードはリスト6-6の match と同じように振る舞います。

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

if let 構文は、等号で区切られたパターンと式を受け取ります。これは match と同じように動作し、式は match に渡され、パターンはその最初のアームになります。この場合、パターンは Some(max) であり、maxSome の内側の値に束縛されます。その後、対応する match アームで max を使ったのと同じように、if let ブロックの本体で max を使えます。if let ブロック内のコードは、値がそのパターンに一致した場合にのみ実行されます。

if let を使うと、書く量が少なくなり、インデントも減り、ボイラープレートコードも少なくなります。しかし、match が強制する、どのケースの処理も忘れていないことを保証する網羅性チェックは失われます。matchif let のどちらを選ぶかは、その状況で何をしているか、そして簡潔さを得ることが網羅性チェックを失うことに対して適切なトレードオフかどうかによって決まります。

言い換えると、if let は、値が1つのパターンに一致したときにコードを実行し、それ以外のすべての値は無視する match の糖衣構文だと考えられます。

if let には else を含めることもできます。else に対応するコードブロックは、if letelse に等価な match 式における _ ケースに対応するコードブロックと同じです。Quarter バリアントが UsState 値も保持していた、リスト6-4の Coin enum 定義を思い出してください。見つけた Quarter 以外の硬貨をすべて数えつつ、同時に25セント硬貨の州を知らせたいとすると、次のような match 式で実現できます。

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

あるいは、次のように if letelse 式を使うこともできます。

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

let...else で“ハッピーパス”を保つ

一般的なパターンは、値が存在するときに何らかの計算を行い、そうでなければデフォルト値を返すことです。UsState 値を持つコインの例を続けると、25セント硬貨の州がどれくらい古いかによって何か面白いことを言いたいなら、次のように州の年齢を確認するメソッドを UsState に導入するかもしれません。

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

そして、リスト6-7のように、コインの種類に対してマッチさせるために if let を使い、条件の本体の中で state 変数を導入するかもしれません。

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

これは目的は果たしますが、処理が if let 文の本体に押し込められており、行うべき処理がもっと複雑であれば、トップレベルの分岐がどのように関係しているのかを正確に追うのが難しくなるかもしれません。式は値を生成するという事実を利用して、if let から state を生成するか、あるいはリスト6-8のように早期リターンすることもできます。(match でも同様のことができます。)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

とはいえ、これにも別の意味で少し追いにくさがあります! if let の一方の分岐は値を生成し、もう一方は関数全体からリターンします。

この一般的なパターンをよりうまく表現するために、Rust には let...else があります。let...else 構文は、左辺にパターン、右辺に式を取り、if let によく似ていますが、if 分岐はなく、else 分岐だけがあります。パターンが一致した場合、パターンの値は外側のスコープに束縛されます。パターンが一致しない場合、プログラムの流れは else アームに入り、そのアームは関数からリターンしなければなりません。

リスト6-9では、if let の代わりに let...else を使ったときに、リスト6-8がどのようになるかを見ることができます。

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

この方法では、if let がそうであったように2つの分岐で大きく異なる制御フローを持つことなく、関数本体の主な処理が“ハッピーパス”に留まっていることに注目してください。

プログラムに、match を使うと冗長すぎて表現しにくいロジックがあるなら、if letlet...else も Rust の道具箱に入っていることを覚えておいてください。

まとめ

ここまでで、列挙された値の集合のいずれかになり得るカスタム型を作るために enum を使う方法を見てきました。標準ライブラリの Option<T> 型が、型システムを使ってエラーを防ぐのに役立つことも示しました。enum の値の内部にデータがある場合は、処理する必要のあるケースの数に応じて、match または if let を使ってその値を取り出して利用できます。

これで、あなたの Rust プログラムは、struct や enum を使ってドメイン内の概念を表現できるようになりました。API で使うカスタム型を作成することで型安全性が確保されます。つまり、各関数が期待する型の値だけをその関数が受け取ることをコンパイラが保証してくれます。

ユーザーに、よく整理され、使いやすく、しかもユーザーが必要とするものだけを正確に公開する API を提供するために、次は Rust のモジュールに目を向けましょう。