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 f = File::open("hello.txt");
}

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

File::openResultを返すとどう知るのでしょうか?標準ライブラリのAPIドキュメントを参照することもできますし、 コンパイラに尋ねることもできます!fに関数の戻り値ではないと判明している型注釈を与えて、 コードのコンパイルを試みれば、コンパイラは型が合わないと教えてくれるでしょう。そして、エラーメッセージは、 f実際の型を教えてくれるでしょう。試してみましょう!File::openの戻り値の型はu32ではないと判明しているので、 let f文を以下のように変更しましょう:

let f: u32 = File::open("hello.txt");

これでコンパイルしようとすると、以下のような出力が得られます:

error[E0308]: mismatched types
(エラー: 型が合いません)
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |                  ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
  |
  = note: expected type `u32`
  (注釈: 予期した型は`u32`です)
             found type `std::result::Result<std::fs::File, std::io::Error>`
  (実際の型は`std::result::Result<std::fs::File, std::io::Error>`です)

これにより、File::open関数の戻り値の型は、Result<T, E>であることがわかります。ジェネリック引数のTは、 ここでは成功値の型std::fs::Fileで埋められていて、これはファイルハンドルです。 エラー値で使用されているEの型は、std::io::Errorです。

この戻り値型は、File::openの呼び出しが成功し、読み込みと書き込みを行えるファイルハンドルを返す可能性があることを意味します。 また、関数呼び出しは失敗もする可能性があります: 例えば、ファイルが存在しない可能性、ファイルへのアクセス権限がない可能性です。 File::openには成功したか失敗したかを知らせる方法とファイルハンドルまたは、エラー情報を与える方法が必要なのです。 この情報こそがResult enumが伝達するものなのです。

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

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

ファイル名: src/main.rs

use std::fs::File;

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

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            // ファイルを開く際に問題がありました
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
}

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

Option enumのように、Result enumとその列挙子は、初期化処理でインポートされているので、 matchアーム内でOkErr列挙子の前にResult::を指定する必要がないことに注目してください。

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

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

thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
('main'スレッドは、src/main.rs:9:12の「ファイルを開く際に問題がありました: Error{ repr:
Os { code: 2, message: "そのような名前のファイルまたはディレクトリはありません"}}」でパニックしました)

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

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

リスト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 f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => {
                    panic!(
                        //ファイルを作成しようとしましたが、問題がありました
                        "Tried to create file but there was a problem: {:?}",
                        e
                    )
                },
            }
        },
        Err(error) => {
            panic!(
                "There was a problem opening the file: {:?}",
                error
            )
        },
    };
}

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

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

if error.kind() == ErrorKind::Notfoundという条件式は、マッチガードと呼ばれます: アームのパターンをさらに洗練するmatchアーム上のおまけの条件式です。この条件式は、 そのアームのコードが実行されるには真でなければいけないのです; そうでなければ、 パターンマッチングは継続し、matchの次のアームを考慮します。パターンのrefは、 errorがガード条件式にムーブされないように必要ですが、ただ単にガード式に参照されます。 refを使用して&の代わりにパターン内で参照を作っている理由は、第18章で詳しく講義します。 手短に言えば、パターンの文脈において、&は参照にマッチし、その値を返しますが、 refは値にマッチし、それへの参照を返すということなのです。

マッチガードで精査したい条件は、error.kind()により返る値が、ErrorKind enumのNotFound列挙子であるかということです。 もしそうなら、File::createでファイル作成を試みます。ところが、File::createも失敗する可能性があるので、 内部にもmatch式を追加する必要があるのです。ファイルが開けないなら、異なるエラーメッセージが出力されるでしょう。 外側の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 f = File::open("hello.txt").unwrap();
}

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

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

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

ファイル名: src/main.rs

use std::fs::File;

fn main() {
    // hello.txtを開くのに失敗しました
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

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

thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

このエラーメッセージは、指定したテキストのhello.txtを開くのに失敗しましたで始まっているので、 コード内のどこでエラーメッセージが出力されたのかより見つけやすくなるでしょう。複数箇所でunwrapを使用していたら、 ズバリどのunwrapがパニックを引き起こしているのか理解するのは、より時間がかかる可能性があります。 パニックするunwrap呼び出しは全て、同じメッセージを出力するからです。

エラーを委譲する

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

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

ファイル名: src/main.rs


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

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

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

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        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に似たmatchで返ってくるResult値を扱い、 Errケースにpanic!を呼び出すだけの代わりに、この関数から早期リターンしてこの関数のエラー値として、 File::openから得たエラー値を呼び出し元に渡し戻します。File::openが成功すれば、 ファイルハンドルを変数fに保管して継続します。

さらに、変数sに新規Stringを生成し、fのファイルハンドルに対してread_to_stringを呼び出して、 ファイルの中身をsに読み出します。File::openが成功しても、失敗する可能性があるので、read_to_stringメソッドも、 Resultを返却します。そのResultを処理するために別のmatchが必要になります: read_to_stringが成功したら、 関数は成功し、今はOkに包まれたsに入っているファイルのユーザ名を返却します。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::io;
use std::io::Read;
use std::fs::File;

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

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

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

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

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

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

ファイル名: src/main.rs


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

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

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

    Ok(s)
}
}

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

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

?演算子は、Resultを返す関数でしか使用できない

?演算子は戻り値にResultを持つ関数でしか使用できません。というのも、リスト9-6で定義したmatch式と同様に動作するよう、 定義されているからです。Resultの戻り値型を要求するmatchの部品は、return Err(e)なので、 関数の戻り値はこのreturnと互換性を保つためにResultでなければならないのです。

main関数で?演算子を使用したらどうなるか見てみましょう。main関数は、戻り値が()でしたね:

use std::fs::File;

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

このコードをコンパイルすると、以下のようなエラーメッセージが得られます:

error[E0277]: the trait bound `(): std::ops::Try` is not satisfied
(エラー: `(): std::ops::Try`というトレイト境界が満たされていません)
 --> src/main.rs:4:13
  |
4 |     let f = File::open("hello.txt")?;
  |             ------------------------
  |             |
  |             the `?` operator can only be used in a function that returns
  `Result` (or another type that implements `std::ops::Try`)
  |             in this macro invocation
  |             (このマクロ呼び出しの`Result`(かまたは`std::ops::Try`を実装する他の型)を返す関数でしか`?`演算子は使用できません)
  |
  = help: the trait `std::ops::Try` is not implemented for `()`
  (助言: `std::ops::Try`トレイトは`()`には実装されていません)
  = note: required by `std::ops::Try::from_error`
  (注釈: `std::ops::Try::from_error`で要求されています)

このエラーは、?演算子はResultを返す関数でしか使用が許可されないと指摘しています。 Resultを返さない関数では、Resultを返す別の関数を呼び出した時、 ?演算子を使用してエラーを呼び出し元に委譲する可能性を生み出す代わりに、matchResultのメソッドのどれかを使う必要があるでしょう。

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

関連キーワード:  エラー, Result, 関数, File, open, 呼び出し, Err, コード, リスト, Ok