🦤

Rustのmatchのネストが深くなりすぎたときの一工夫

2023/08/24に公開2

最近またRustを勉強し始めています。

気がついたらmatchのネストが深くなりすぎていた

Resultの処理をするのにmatchが便利です。
でもOkのときに続きの処理を書いていったら、ネストが深くなりすぎてしまいました。
こんな感じ。

    loop {
        match reader.read_ivf_frame_header() {
            Ok(frame_header) => {
                let len: usize = frame_header.frame_size as _;
                match reader.read_frame(&mut frame_buffer[..len]) {
                    Ok(_) => {}
                    Err(ref e) if e.kind() == ErrorKind::UnexpectedEof => break,
                    Err(e) => {
                        eprintln!("Error: {e:?}");
                        break;
                    }
                }
                match vp8dec.decode(&frame_buffer[..len]) {
                    Ok(raw_video) => match outfile.write_all(raw_video) {
                        Ok(_) => {}
                        Err(ref e) if e.kind() == ErrorKind::BrokenPipe => break,
                        Err(e) => {
                            eprintln!("Error: {e:?}");
                            break;
                        }
                    },
                    Err(e) => {
                        eprintln!("Error: {e:?}");
                        break;
                    }
                }
            }
            Err(e) => {
                eprintln!("Error: {e:?}");
                break;
            }
        }
        frame_index += 1;
    }

match が3段にネストしてしまって、インデントの対応も見づらくなっています。
これでは見通しが悪いので、ネストが深くならないように書き直したいですね。

and_then を試す

https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then

やってみたけど、今回の場合はダメでした。クロージャの中からはbreakは使えないということに、コンパイルエラーになってから気がつきました。

unwrap, expect を使う

エラーのときには、そのエラーの種別も見ずにpanicさせるなら、unwrapexpectを使うことですっきりさせることもできますが、今回は見送りました。

    let var = some_func().expect("Failed in some_func()");

let var = match ... を使う

ネストが深くなっているのは、Ok(var)のところに、正常系の続きの処理を埋め込んでしまっているためです。
ネストが深くならないようにするためには、matchが正常のときの値を返すようにし、それをmatchの外側で取り出します。

    let var = match some_func() {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Error: {e:?}");
            break;
        }
    };

このように Ok(f) => f とすれば、正常の値をmatchの外に持っていけます。
今回の場合はErrの場合は全て、break, continue, returnのような制御構文になっているのでこれでうまくいきました。

書き直した結果

    loop {
        let frame_header = match reader.read_ivf_frame_header() {
            Ok(f) => f,
            Err(e) => {
                eprintln!("Error: {e:?}");
                break;
            }
        };
        let len: usize = frame_header.frame_size as _;
        match reader.read_frame(&mut frame_buffer[..len]) {
            Ok(_) => {}
            Err(ref e) if e.kind() == ErrorKind::UnexpectedEof => break,
            Err(e) => {
                eprintln!("Error: {e:?}");
                break;
            }
        }
        let raw_video = match vp8dec.decode(&frame_buffer[..len]) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("Error: {e:?}");
                break;
            }
        };
        match outfile.write_all(raw_video) {
            Ok(_) => {}
            Err(ref e) if e.kind() == ErrorKind::BrokenPipe => break,
            Err(e) => {
                eprintln!("Error: {e:?}");
                break;
            }
        }
        frame_index += 1;
    }

ネストが深くなりすぎずに処理の順を追うのが楽になりました。

let-else

https://doc.rust-lang.org/rust-by-example/flow_control/let_else.html

Errの処理が1種類しかない場合にはlet-elseの構文が使えるかなと思ったのですが、これだとErrの種別がわからないですね。今回は見送りです。

Discussion

とがとが

このような状況だと,ループ内部を関数にして ? 演算子を使うという方法も適している気がしますが,いかがでしょうか?