Resultで回復可能なエラー

多くのエラーは、プログラムを完全にストップさせるほど深刻ではありません。時々、関数が失敗した時に、 容易に解釈し、対応できる理由によることがあります。例えば、ファイルを開こうとして、 ファイルが存在しないために処理が失敗したら、プロセスを停止するのではなく、ファイルを作成したいことがあります。

第2章のResultで失敗の可能性を扱う」Result enumが以下のように、 OkErrの2列挙子からなるよう定義されていることを思い出してください:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

TEは、ジェネリックな型引数です: ジェネリクスについて詳しくは、第10章で議論します。 たった今知っておく必要があることは、Tが成功した時にOk列挙子に含まれて返される値の型を表すことと、 Eが失敗した時にErr列挙子に含まれて返されるエラーの型を表すことです。Resultはこのようなジェネリックな型引数を含むので、 Result型とその上に定義されている関数を、成功した時とエラーの時に返したい値が異なるような様々な場面で使用できるのです。

関数が失敗する可能性があるためにResult値を返す関数を呼び出しましょう: リスト9-3では、 ファイルを開こうとしています。

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

リスト9-3: ファイルを開く

File::openの戻り値の型は、Result<T, E>です。ジェネリック引数のTは、 File::openの実装によって成功値の型std::fs::Fileで埋められていて、これはファイルハンドルです。 エラー値で使用されているEの型は、std::io::Errorです。 この戻り値型は、File::openの呼び出しが成功し、読み込みと書き込みを行えるファイルハンドルを返す可能性があることを意味します。 また、関数呼び出しは失敗もする可能性があります: 例えば、ファイルが存在しない可能性、ファイルへのアクセス権限がない可能性です。 File::openには成功したか失敗したかを知らせる方法とファイルハンドルまたは、エラー情報を与える方法が必要なのです。 この情報こそがResult enumが伝達するものなのです。

File::openが成功した場合、変数greeting_file_resultの値はファイルハンドルを含むOkインスタンスになります。 失敗した場合には、発生したエラーの種類に関する情報をより多く含むErrインスタンスがgreeting_file_resultの値になります。

リスト9-3のコードに追記をしてFile::openが返す値に応じて異なる動作をする必要があります。 リスト9-4に基礎的な道具を使ってResultを扱う方法を一つ示しています。第6章で議論したmatch式です。

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        //                   "ファイルを開くのに問題がありました: {:?}"
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

リスト9-4: match式を使用して返却される可能性のあるResult列挙子を処理する

Option enumのように、Result enumとその列挙子は、preludeでスコープ内に持ち込まれているので、 matchアーム内でOkErr列挙子の前にResult::を指定する必要がないことに注目してください。

このコードは、結果がOkの場合は、Ok列挙子から中身のfile値を返し、 それからそのファイルハンドル値を変数greeting_fileに代入しています。 matchの後には、ファイルハンドルを使用して読み込んだり書き込むことができるわけです。

matchのもう一つのアームは、File::openからErr値が得られたケースを処理しています。 この例では、panic!マクロを呼び出すことを選択しています。カレントディレクトリにhello.txtというファイルがなく、 このコードを走らせたら、panic!マクロからの以下のような出力を目の当たりにするでしょう:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
('main'スレッドは、src/main.rs:8:23でパニックしました:
ファイルを開く際に問題がありました: Os { code: 2, kind: NotFound, message: "そのようなファイルやディレクトリはありません" })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

通常通り、この出力は、一体何がおかしくなったのかを物語っています。

色々なエラーにマッチする

リスト9-4のコードは、File::openが失敗した理由にかかわらずpanic!します。 ですが、失敗理由によって動作を変えたいとしましょう: ファイルが存在しないためにFile::openが失敗したら、 ファイルを作成し、その新しいファイルへのハンドルを返したいです。他の理由(例えばファイルを開く権限がなかったなど)で、 File::openが失敗したら、リスト9-4のようにコードにはpanic!してほしいのです。 このために、リスト9-5で示すように内側のmatch式を追加します。

ファイル名: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                //               "ファイルを作成するのに問題がありました: {:?}"
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                //     "ファイルを開くのに問題がありました: {:?}"
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

リスト9-5: 色々な種類のエラーを異なる方法で扱う

File::openErr列挙子に含めて返す値の型は、io::Errorであり、これは標準ライブラリで提供されている構造体です。 この構造体には、呼び出すとio::ErrorKind値が得られるkindメソッドがあります。io::ErrorKindというenumは、 標準ライブラリで提供されていて、io処理の結果発生する可能性のある色々な種類のエラーを表す列挙子があります。 使用したい列挙子は、ErrorKind::NotFoundで、これは開こうとしているファイルがまだ存在しないことを示唆します。 そこで、greeting_file_resultに対してマッチし、さらにerror.kind()に対する内側のマッチも持たせています。

内側のマッチで精査したい条件は、error.kind()により返る値が、ErrorKind enumのNotFound列挙子であるかということです。 もしそうなら、File::createでファイル作成を試みます。ところが、File::createも失敗する可能性があるので、 内側のmatch式の2番目のアームが必要なのです。ファイルを作成できない場合、異なるエラーメッセージが出力されます。 外側のmatchの2番目のアームは同じままなので、ファイルが存在しないエラー以外ならプログラムはパニックします。

Result<T, E>に対するmatchの使用の代わりとなる方法

matchがたくさん出てきましたね!match式は非常に有用ですが、非常に原始的でもあります。 第13章でクロージャについて学習しますが、これはResult<T, E>上に定義されているメソッドの多くで使用することができます。 コード内でResult<T, E>値を扱うときには、こうしたメソッドを使ったほうがより簡潔になります。

例えば、以下はリスト9-5に示したものと同じロジックを書く例ですが、 今度はクロージャとunwrap_or_elseメソッドを使用しています:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

このコードはリスト9-5と同じ動作をしますが、match式をまったく含んでおらず、より読みやすいです。 第13章を読み終えたら、この例に戻ってきて、標準ライブラリドキュメント内でunwrap_or_elseを探してみてください。 エラーに対処するときには、これらのメソッドを多用することで、巨大なネストされたmatch式を整理することができます。

エラー時にパニックするショートカット: unwrapexpect

matchの使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。 Result<T, E>型には、様々な特定の作業をするヘルパーメソッドが多く定義されています。 unwrapメソッドは、リスト9-4で書いたmatch式と同じように実装された短絡メソッドです。 Result値がOk列挙子なら、unwrapOkの中身を返します。ResultErr列挙子なら、 unwrappanic!マクロを呼んでくれます。こちらが実際に動作しているunwrapの例です:

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

このコードをhello.txtファイルなしで走らせたら、unwrapメソッドが行うpanic!呼び出しからのエラーメッセージを目の当たりにするでしょう:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
('main'スレッドは、src/main.rs:4:49でパニックしました:
`Err`値に対して`Result::unwrap()`が呼び出されました: Os { code: 2, kind: NotFound, message: "そのようなファイルやディレクトリはありません" })

同様に、expectメソッドは、panic!のエラーメッセージも選択させてくれます。 unwrapの代わりにexpectを使用して、いいエラーメッセージを提供すると、意図を伝え、 パニックの原因をたどりやすくしてくれます。expectの表記はこんな感じです:

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
    	//      "hello.txtがこのプロジェクトに含まれているべきです"
        .expect("hello.txt should be included in this project");
}

expectunwrapと同じように使用してます: ファイルハンドルを返したり、panic!マクロを呼び出しています。 expectpanic!呼び出しで使用するエラーメッセージは、unwrapが使用するデフォルトのpanic!メッセージではなく、 expectに渡した引数になります。以下のようになります:

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
('main'スレッドは、src/main.rs:5:10でパニックしました:
hello.txtがこのプロジェクトに含まれているべきです: Os { code: 2, kind: NotFound, message: "そのようなファイルやディレクトリはありません" })

製品レベルの品質のコードでは、多くのRustaceanはunwrapよりむしろexpectを選択し、 なぜその操作が常に成功すると想定されるのかについてより多くの文脈情報を提供します。 そうすることで、万一あなたの仮定が誤っていたと判明した場合には、 デバッグ時に利用するためのより多くの情報が得られます。

エラーを委譲する

関数の実装が失敗する可能性のある何かを呼び出す際、その関数自体の中でエラーを処理する代わりに、 呼び出し元がどうするかを決められるようにエラーを返すことができます。これはエラーの委譲として認知され、 自分のコードの文脈で利用可能なものよりも、 エラーの処理法を規定する情報やロジックがより多くある呼び出し元のコードに制御を明け渡します。

例えば、リスト9-6の関数は、ファイルからユーザ名を読み取ります。ファイルが存在しなかったり、読み込みできなければ、 この関数はそのようなエラーを呼び出し元のコードに返します。

ファイル名: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}

リスト9-6: matchでエラーを呼び出し元のコードに返す関数

この関数はもっと短く書くことができますが、エラー処理について詳しく見るために、 まずはエラー処理をたくさん手動で書くことから始めましょう; より短い方法は最後に示します。 まずは、関数の戻り値型に注目しましょう: Result<String, io::Error>です。つまり、この関数は、 Result<T, E>型の値を返しているということです。ここでジェネリック引数のTは、具体型Stringで埋められ、 ジェネリック引数のEは具体型io::Errorで埋められています。

この関数が何の問題もなく成功すれば、 この関数を呼び出したコードは、String(関数がファイルから読み取ったユーザ名)を保持するOk値を受け取ります。 この関数が何か問題に行き当たったら、呼び出し元のコードはio::Errorのインスタンスを保持するErr値を受け取り、 このio::Errorは問題の内容に関する情報をより多く含んでいます。関数の戻り値の型にio::Errorを選んだのは、 この関数本体で呼び出している失敗する可能性のある処理が両方とも偶然この型をエラー値として返すからです: File::open関数とread_to_stringメソッドです。

関数の本体は、File::open関数を呼び出すところから始まります。 そして、リスト9-4のmatchに似たmatchResult値を扱います。 File::openが成功すれば、パターン変数fileにあるファイルハンドルは可変変数username_file内の値となり、 関数は継続します。 Errケースではpanic!を呼び出す代わりに、関数から完全に早期リターンしてこの関数のエラー値として、 File::openから得たエラー値、これはパターン変数e内にありますが、これを呼び出し元に渡し戻すためにreturnキーワードを使用します。

username_fileにファイルハンドルが得られたら、関数は次に変数usernameに新規Stringを生成し、 username_fileのファイルハンドルに対してread_to_stringを呼び出して、ファイルの中身をusernameに読み出します。 File::openが成功しても、失敗する可能性があるので、read_to_stringメソッドも、 Resultを返却します。そのResultを処理するために別のmatchが必要になります: read_to_stringが成功したら、 関数は成功し、今はOkに包まれたusernameに入っているファイルのユーザ名を返却します。read_to_stringが失敗したら、 File::openの戻り値を扱ったmatchでエラー値を返したように、エラー値を返します。 しかし、明示的にreturnを述べる必要はありません。これが関数の最後の式だからです。

そうしたら、呼び出し元のコードは、ユーザ名を含むOk値か、io::Errorを含むErr値を得て扱います。 それらの値をどうするかを決めるのは、呼び出し元のコードに委ねられます。呼び出しコードがErr値を得たら、 例えば、panic!を呼び出してプログラムをクラッシュさせたり、デフォルトのユーザ名を使ったり、 ファイル以外の場所からユーザ名を検索したりできるでしょう。呼び出し元のコードが実際に何をしようとするかについて、 十分な情報がないので、成功や失敗情報を全て委譲して適切に扱えるようにするのです。

Rustにおいて、この種のエラー委譲は非常に一般的なので、Rustにはこれをしやすくする?演算子が用意されています。

エラー委譲のショートカット: ?演算子

リスト9-7はリスト9-6にあったのと同じ機能を持つread_username_from_fileの実装ですが、 こちらは?演算子を使用しています。

ファイル名: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}

リスト9-7: ?演算子でエラーを呼び出し元に返す関数

Result値の直後に置かれた?は、リスト9-6でResult値を処理するために定義したmatch式とほぼ同じように動作します。 Resultの値がOkなら、Okの中身がこの式から返ってきて、プログラムは継続します。値がErrなら、 returnキーワードを使ったかのように関数全体からErrが返ってくるので、 エラー値は呼び出し元のコードに委譲されます。

リスト9-6のmatch式がやっていることと?演算子がやっていることには、違いがあります: ?演算子が呼ばれる対象となったエラー値は、 標準ライブラリのFromトレイトで定義され、値を別の型に変換するfrom関数を通ることです。 ?演算子がfrom関数を呼び出すと、受け取ったエラー型が現在の関数の戻り値型で定義されているエラー型に変換されます。これは、 個々がいろんな理由で失敗する可能性があるのにも関わらず、関数が失敗する可能性を全て一つのエラー型で表現して返す時に有用です。

例えば、リスト9-7のread_username_from_file関数を、OurErrorという名前の自分で定義したカスタムエラー型を返すように変更することができます。 さらに、io::ErrorからOutErrorのインスタンスを構築するためのimpl From<io::Error> for OurErrorを定義すれば、 read_username_from_fileの本体の中の?演算子呼び出しはfromを呼び出してくれるので、 関数にコードを追加する必要なくエラー型を変換してくれます。

リスト9-7の文脈では、File::open呼び出し末尾の?Okの中身を変数username_fileに返します。 エラーが発生したら、?演算子により関数全体から早期リターンし、あらゆるErr値を呼び出し元に与えます。 同じ法則がread_to_string呼び出し末尾の?にも適用されます。

?演算子により定型コードの多くが排除され、この関数の実装を単純にしてくれます。 リスト9-8で示したように、?の直後のメソッド呼び出しを連結することでさらにこのコードを短くすることさえもできます。

ファイル名: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}

リスト9-8: ?演算子の後のメソッド呼び出しを連結する

usernameの新規Stringの生成を関数の冒頭に移動しました; その部分は変化していません。 変数username_fileを生成する代わりに、 read_to_stringの呼び出しを直接File::open("hello.txt")?の結果に連結させました。 それでも、read_to_string呼び出しの末尾には?があり、File::openread_to_string両方が成功したら、 エラーを返すというよりもそれでも、usernameを含むOk値を返します。機能もまたリスト9-6及び、9-7と同じです; ただ単に異なるバージョンのよりエルゴノミックな書き方なのです。

これをfs::read_to_stringを使用してさらに短くする方法をリスト9-9に示します。

ファイル名: src/main.rs

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

リスト9-9: ファイルを開いて読む代わりにfs::read_to_stringを使用する

ファイルを文字列に読み込むことは非常によくある操作なので、標準ライブラリは、 ファイルを開き、新しいStringを作成し、ファイルの内容を読み込み、内容をそのStringに格納し、それを返す、 便利なfs::read_to_string関数を提供しています。そうそう、fs::read_to_stringを使用してしまうと、 エラー処理のすべてについて説明する機会がなくなってしまうので、長ったらしい方法を先に使ったのでした。

?演算子が使用できる場所

?演算子は、?を使用する対象の値と戻り値の型に互換性がある関数でしか使用できません。 というのも?演算子は、リスト9-6で定義したmatch式と同様に関数から値の早期リターンを実行するように定義されているからです。 リスト9-6では、matchResult値を使用して、早期リターンする側のアームはErr(e)値を返していました。 関数の戻り値型はこのreturnと互換性を保てるように、Resultでなければならないのです。

リスト9-10で、?を使用する対象の値の型と戻り値の型に互換性がないmain関数で、 ?演算子を使用した場合に得られるエラーを見てみましょう:

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

リスト9-10: ()を返すmain関数内で?を使用しようとするとコンパイルが通らない

このコードはファイルを開きますが、これは失敗する可能性があります。 ?演算子はFile::openによって返されるResult値を確認して対処しますが、このmain関数はResultではなく()の戻り値型を持っています。 このコードをコンパイルすると、以下のようなエラーメッセージが得られます:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
(エラー: `?`演算子は`Result`または`Option`(あるいはその他`FromResidual`を実装する型)を返す関数内でのみ使用できます
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
             (`?`を使えるようにするには、この関数は`Result`または`Option`を返すべきです)
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
                                                    (`()`を返す関数内では`?`演算子を使用できません)
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
   (ヘルプ: トレイト`FromResidual<Result<Infallible, std::io::Error>>`は`()`に対して実装されていません)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

このエラーは、?演算子はResultOption、またはその他FromResidualを実装する型を返す関数でしか使用が許可されないと指摘しています。

このエラーを修正する方法としては、二つの選択肢があります。 一つは、それを阻害する制限がない場合は、関数の戻り値型を、?演算子を使用する対象の値と互換性があるような型に変更することです。 もう一つの手法は、matchまたはResult<T, E>のメソッドのいずれかを使用して、何らかの適切な方法でResult<T, E>を処理することです。

エラーメッセージは、?Option<T>値にも使用できることに言及しています。 Result?を使用する場合と同様に、Optionを返す関数の中でのみ、Option?を使用することができます。 Option<T>に対して呼ばれた場合の?演算子の挙動は、Result<T, E>に対して呼ばれた場合の挙動と似ています: もしその値がNoneならば、その時点でNoneが関数から早期リターンされます。 もしその値がSomeならば、Someの内側の値がその式の結果の値となり、関数は継続します。 リスト9-11は、与えられたテキストの最初の行の最後の文字を探索する関数の例です:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}

リスト9-11: Option<T>値に対する?演算子の使用

この関数は、文字はあるかもしれないし、ないかもしれないので、Option<char>を返します。 このコードはtext文字列スライス引数を受け取り、それに対してlinesメソッドを呼び出します。 これは文字列内の行を走査するイテレータを返します。この関数では最初の行を確認したいので、 イテレータに対してnextを呼び出してイテレータから最初の値を取得します。 textが空文字列の場合は、このnext呼び出しはNoneを返すでしょう。 その場合は?を使用して停止し、last_char_of_first_lineからNoneを返します。 textが空文字列でない場合は、nexttext内の最初の行の文字列スライスを含むSome値を返すでしょう。

?が文字列スライスを抽出するので、その文字列スライスに対してcharsを呼び出して、含まれる文字からなるイテレータを得ることができます。 最初の行の最後の文字に関心があるので、このイテレータの最後の要素を返すためにlastを呼び出します。 例えばtextが空行で始まるが他の行には文字が含まれる"\nhi"のような文字列の場合、 最初の行が空文字列である可能性があるので、この戻り値はOptionになっています。 しかしながら、最初の行の最後の文字がある場合は、それをSome列挙子に入れて返します。 中間の?演算子はこのロジックを簡潔に表現する方法を提供し、そのためこの関数を一行で実装することができています。 もし?演算子がOptionに使用できなかったなら、より多くのメソッド呼び出しやmatch式を使用してこのロジックを実装する必要があったでしょう。

Resultを返す関数内ではResultに対して?演算子を使用でき、Optionを返す関数内ではOptionに対して?演算子を使用できますが、 混ぜて組み合わせることはできないことに注意してください。 ?演算子は、ResultOptionに、またはその逆に、自動的に変換することはしません; そのような場合には明示的に変換を行うために、Resultokメソッドや、Optionok_orメソッドのようなメソッドを使用することができます。

今までに使ってきたmain関数はすべて()を返してきました。 main関数は、実行可能なプログラムの開始および終了点であることから特別扱いされており、 プログラムが期待通りに振る舞うために、その戻り値型として指定できる型に制限があります。

幸運なことに、mainResult<(), E>を返すこともできます。 リスト9-12はリスト9-10のコードを含んでいますが、mainの戻り値型をResult<(), Box<dyn Error>>に変更し、 最後に戻り値Ok(())を追加しています。 これでこのコードはコンパイルできるでしょう:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

リスト9-12: Result<(), E>を返すようにmainを変更することで、Result値に対する?演算子が使用可能になる

Box<dyn Error>型はトレイトオブジェクトですが、これについては第17章の「トレイトオブジェクトで異なる型の値を許容する」節で話します。 今のところは、Box<dyn Error>は「任意の種類のエラー」を意味するものと理解してください。 エラー型Box<dyn Error>を持つmain関数の中では、任意のErr値を早期リターンすることができるので、Result値に対する?の使用が許可されます。 このmain関数の本体は型std::io::Errorのエラーしか返しませんが、それでもBox<dyn Error>を指定することにより、 他のエラーを返すコードがmainの本体に追加された場合であっても、このシグネチャは正しいままであり続けるでしょう。

main関数がResult<(), E>を返す場合、その実行可能ファイルは、 mainOk(())を返す場合には値0で終了し、mainErr値を返す場合は非ゼロ値で終了するでしょう。 Cで書かれた実行可能ファイルは終了時に整数を返します: 正常終了するプログラムは整数0を返し、エラーが発生したプログラムは0以外の整数を返します。 Rustもこの慣例に従い、実行可能ファイルから整数を返します。

main関数はstd::process::Terminationトレイトを実装する任意の型を返すことができます。 このトレイトにはExitCodeを返すreport関数が含まれます。 自身の型に対するTerminationトレイトの実装に関するさらなる情報については、 標準ライブラリドキュメントを確認してください。

さて、panic!呼び出しやResultを返す詳細について議論し終えたので、 どんな場合にどちらを使うのが適切か決める方法についての話に戻りましょう。