Apache License Version 2.0
Error Handling
RustはエラーをRecoverableとUnrecoverableに分けている.前者は生じたことをユーザーに知らせてインプットし直させたりして解決しうるエラーであり,後者はarrayの長さよりも大きいindexを指定するような,回復不能なエラーである.
RustではResult<T, E>
型によって前者の発生を伝え,後者の場合panic!
macro が実行を停止する.この章ではまずpanic!
の扱い方を論じてからResult<T, E>
を論じる.さらに,エラーから復帰するか停止するかを決めるに当たっての方法論を述べる.
Unrecoverable Errors with panic!
バグが生じて,プログラムがそれをどう処理するかわからないとき,panic!
macroはエラーメッセージを出力し,メモリをきれいにしてから実行を停止する.
Unwinding the Stack Versus Aborting on Panic
デフォルトではpanic!
によってプログラムはunwinding(解きほぐし?)を始める.unwindingはRustの関数が持っていたデータを削除することである.これには時間がかかるので,ただちにabortしてメモリをそのままにプログラムを停止することもできる.この場合OSがメモリを掃除することになる.プログラムのサイズをできるだけ小さくしたいときはcargo.tomlの[profile]
にpanic = 'abort'
を追加することでabortを指定できる.
試しにpanic!
を呼んでみよう.
src/main.rs
fn main() {
panic!("crash and burn");
}
shell
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/error_handling`
thread 'main' panicked at 'crash and burn', src/main.rs:2
note: Run with `RUST_BACKTRACE=1` for a backtrace.
エラーメッセージが示すsrc/main.rsの2行目には我々が書いたpanic!
があるが,普通のプログラムではエラーメッセージが示している部分を更に我々が書いたコードが呼んでいることが多いので,その場合にはbacktraceによって,我々のコードが孕んでいるバグを見つけることができる.
Using a panic!
backtrace
src/main.rs
fn main() {
let v = vec![1, 2, 3];
v[100];
}
このコードはv
というVectorの割り当てられた範囲外のメモリを参照している.
Cのような言語はこうしたコードを無事コンパイルして,実行時にbufer overreadという危険な状態が生じる.Rustではunrecoverableなエラーを生じ,panic!
する.
shell
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/error_handling`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /checkout/src/libcollections/vec.rs:1488
note: Run with `RUST_BACKTRACE=1` for a backtrace.
このエラーメッセージにはlibcollections/vec.rsというファイルが含まれる.このファイルでRustはVec<T>
型を実装していて,[]
をv
に対して使うときに呼び出される.panic!
は実際にはここで起こっているのである.
最終行ではRUST_BACKTRACE
を有効にすることでbacktraceを行えることがわかる.実際にやってみよう.
shell
ren@ren-ThinkCentre-Edge72:~/Projects/error_handling$ RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/error_handling`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /checkout/src/libcollections/vec.rs:1488
stack backtrace:
0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
1: std::sys_common::backtrace::_print
at /checkout/src/libstd/sys_common/backtrace.rs:71
2: std::panicking::default_hook::{{closure}}
at /checkout/src/libstd/sys_common/backtrace.rs:60
at /checkout/src/libstd/panicking.rs:355
3: std::panicking::default_hook
at /checkout/src/libstd/panicking.rs:371
4: std::panicking::rust_panic_with_hook
at /checkout/src/libstd/panicking.rs:549
5: std::panicking::begin_panic
at /checkout/src/libstd/panicking.rs:511
6: std::panicking::begin_panic_fmt
at /checkout/src/libstd/panicking.rs:495
7: rust_begin_unwind
at /checkout/src/libstd/panicking.rs:471
8: core::panicking::panic_fmt
at /checkout/src/libcore/panicking.rs:69
9: core::panicking::panic_bounds_check
at /checkout/src/libcore/panicking.rs:56
10: <collections::vec::Vec<T> as core::ops::Index<usize>>::index
at /checkout/src/libcollections/vec.rs:1488
11: error_handling::main
at ./src/main.rs:3
12: __rust_maybe_catch_panic
at /checkout/src/libpanic_unwind/lib.rs:98
13: std::rt::lang_start
at /checkout/src/libstd/panicking.rs:433
at /checkout/src/libstd/panic.rs:361
at /checkout/src/libstd/rt.rs:57
14: main
15: __libc_start_main
16: _start
11行目で,src/main.rsの3行目がエラーに関係していることを教えてくれる.
Recoverable Errors with Result
殆どのエラーはプログラムをただちに終了させるほどのものではなく,例えば開くファイルをユーザーが指定するときに存在しないパスを入力してしまったときのような,もう一度正しいインプットをするように促すだけですむものもある.Chap.2 でみたように,Result
型を使ってこのような状況を扱うことが出来る.Result
はOk
かErr
の値を取るEnum
であって,以下のように定義されている.
enum Result<T, E> {
Ok(T),
Err(E),
}
T
とE
はgeneric type parameterといって,Chap.10で詳しく述べる.今は,T
の場合にOk
とともに返す値の型が,E
の場合にErr
とともに返す値の型を表すと考えれば良い.
Result
型を返す関数を使ってみよう.
src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
読み出しに成功するとf
はOk
のインスタンスで,ファイルへのhandleを持つことになり,失敗するとErr
のインスタンスでエラーの詳細を持つことになる.
Result
によって挙動を変えるときには以下のようにする.
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 \n : {:?}", error)
},
};
}
match
構文によってf
の型で場合分けし,正常な場合はf
にハンドラを改めて代入し,異常な場合はpanic!
する.このとき,以下のようなエラーが出力される.
shell
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
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Matching on Different Errors
先程はOk
かErr
かのみで分岐したが,Err
の内容によってさらに分岐することも出来る.例えば,File::open
が,開くファイルが存在しないためにエラーを出した場合,新しくそのファイルを作れば復帰できる一方で,開くファイルへのパーミッションを持っていないときにはpanic!
したいとする.このときは,以下のように書く.
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
}
},
}
},
};
}
Propagating Errors
関数が別の何かを呼んで,その何かがエラーを生じたときに,もとの関数がそのエラーの内容によって分岐するように出来る.これをpropagatingといい,もとの関数がエラーをどう処理するかを関数の他の内部状態で決めたいときに使われる.
以下のコードでread_username_from_file
を呼んだ関数はResult
を返される.
src/main.rs, list9-5
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),
}
}
fn main() {
let result = read_username_from_file();
println!("The return result is \n : {:?}", result);
}
shell
Running `target/debug/error_handling`
The return result is
: Err(Error { repr: Os { code: 2, message: "No such file or directory" } })
簡潔に書く方法に?
キーワードを使う方法が有る.
A Shortcut for Propagating Error: ?
先ほどと同じ機能を持つ関数を?
を使って書く.
src/main.rs, list9-6
use std::io;
use std::io::Read;
use std::io::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
型の後に?
がつくと,Ok
の場合はその中身を返し,関数を継続する.Err
の場合はそれを呼んだもとに返してただちに関数を終了する.
list9-6はさらに簡潔に書ける.
src/main.rs, list9-7
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)
}
?
Can Only Be Used in Functions That Return Result
Err
に?
をつけると,Err
自体がその関数の返す値になるので,?
キーワードを使う関数は最初から返り値の型がResult
でなければならない.
To panic!
or Not To panic!
多くの場合panic!
でプログラムを落とすより,条件分岐を使って通常の状態に復帰することを考えるべきだが,panic!
を使うべき場面もいくつか存在する.
Examples, Prototype code, and Test: Perfectly Fine to Panic
サンプルコードや青写真を書くときには,高度なエラーの取扱はロジックをわかりにくくする恐れが有るし,unwrap
やexpect
のほうが明瞭にどこでエラーが起きたかわかりやすい場合が有る.テストを行う場合も同様である.
Cases When You Have More Infromation Than The Compiler
必ずResult
がOk
となるような仕組みが有るときは,unwrap
かexpect
を使ってErr
の場合の処理を書くのを省略できる.例えば
use std::net::IpAddr;
let home = "127.0.0.1".parse::<IpAddr>().unwrap();
とする.”127.0.0.1”は有効なIPアドレスだからparseは成功するはずだが,parseは本質的にResultを返すmethodだから,Err
が返されたときの処理も書かないとコンパイラに怒られる.これを回避して簡潔に書くため,単にunwrap
を使える.
仮にIPアドレスをユーザーが入力するプログラムなら,無効なIPアドレスを入力される場合が考えられるので,Result
によってエラー処理を書くほうが良い.