Result
で回復可能なエラー
多くのエラーは、プログラムを完全にストップさせるほど深刻ではありません。時々、関数が失敗した時に、 容易に解釈し、対応できる理由によることがあります。例えば、ファイルを開こうとして、 ファイルが存在しないために処理が失敗したら、プロセスを停止するのではなく、ファイルを作成したいことがあります。
第2章の「Result
型で失敗する可能性に対処する」でResult
enumが以下のように、
Ok
とErr
の2列挙子からなるよう定義されていることを思い出してください:
# #![allow(unused_variables)] #fn main() { enum Result<T, E> { Ok(T), Err(E), } #}
T
とE
は、ジェネリックな型引数です: ジェネリクスについて詳しくは、第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"); }
File::open
がResult
を返すとどう知るのでしょうか?標準ライブラリの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) }, }; }
Option
enumのように、Result
enumとその列挙子は、初期化処理でインポートされているので、
match
アーム内でOk
とErr
列挙子の前に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
)
},
};
}
File::open
がErr
列挙子に含めて返す値の型は、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
の最後のアームは同じままなので、ファイルが存在しないエラー以外ならプログラムはパニックします。
エラー時にパニックするショートカット: unwrap
とexpect
match
の使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。
Result<T, E>
型には、色々な作業をするヘルパーメソッドが多く定義されています。それらの関数の一つは、
unwrap
と呼ばれますが、リスト9-4で書いたmatch
式と同じように実装された短絡メソッドです。
Result
値がOk
列挙子なら、unwrap
はOk
の中身を返します。Result
がErr
列挙子なら、
unwrap
はpanic!
マクロを呼んでくれます。こちらが実際に動作している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"); }
expect
をunwrap
と同じように使用してます: ファイルハンドルを返したり、panic!
マクロを呼び出しています。
expect
がpanic!
呼び出しで使用するエラーメッセージは、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_variables)] #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), } } #}
まずは、関数の戻り値型に注目してください: 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_variables)] #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) } #}
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_variables)] #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) } #}
s
の新規String
の生成を関数の冒頭に移動しました; その部分は変化していません。変数f
を生成する代わりに、
read_to_string
の呼び出しを直接File::open("hello.txt")?
の結果に連結させました。
それでも、read_to_string
呼び出しの末尾には?
があり、File::open
とread_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
を返す別の関数を呼び出した時、
?
演算子を使用してエラーを呼び出し元に委譲する可能性を生み出す代わりに、match
かResult
のメソッドのどれかを使う必要があるでしょう。
さて、panic!
呼び出しやResult
を返す詳細について議論し終えたので、
どんな場合にどちらを使うのが適切か決める方法についての話に戻りましょう。