2017年7月9日日曜日

The Rust Programming Language 2nd 12日目 Error Handling

https://doc.rust-lang.org/book/second-edition/
Apache License Version 2.0

Error Handling

RustはエラーをRecoverableUnrecoverableに分けている.前者は生じたことをユーザーに知らせてインプットし直させたりして解決しうるエラーであり,後者は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型を使ってこのような状況を扱うことが出来る.ResultOkErrの値を取るEnumであって,以下のように定義されている.

enum Result<T, E> {
  Ok(T),
  Err(E),
}

TEは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");
    // File::openはResult型を返す.
    // 正常に読み出すとResult<T>:std::fs::File
    // 読み出しでエラーが生じるとReulst<E>std::io::Error型が入る
}

読み出しに成功するとfOkのインスタンスで,ファイルへの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

先程はOkErrかのみで分岐したが,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,                 // fにファイルへのハンドラを代入
    Err(e) => return Err(e),          // Err(io::Error)
  };

  let mut s = String::new();

  match f.read_to_string(&mut s) {    // sにfの中身を(あれば)代入
    Ok(_) => Ok(s),                   // Ok(String)
    Err(e) => Err(e),                 // Err(io::Error)
  }
  // 返される値はResult型であり,呼んだ関数がエラー処理を行うことになる.
}

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")?;
              // Okならfは中身(ハンドラ)をfに代入し継続
              // Errなら関数を終了してResultを呼んだもとに返す

  let mut s = String::new();
  f.read_to_string(&mut s)?;
              // Ok ならstatementはread_to_stringのOkの中身
              // Errなら関数を終了してResultを呼んだもとに返す
  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

サンプルコードや青写真を書くときには,高度なエラーの取扱はロジックをわかりにくくする恐れが有るし,unwrapexpectのほうが明瞭にどこでエラーが起きたかわかりやすい場合が有る.テストを行う場合も同様である.

Cases When You Have More Infromation Than The Compiler

必ずResultOkとなるような仕組みが有るときは,unwrapexpectを使って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によってエラー処理を書くほうが良い.

0 件のコメント:

コメントを投稿