3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust勉強中 - その13 -> 式とエラーハンドリング

Last updated at Posted at 2019-10-12

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回は参照とライフタイムについて学びました。
Rust勉強中 - その12

式言語

Rustは式言語だそうです。式は値を生成し、文は値を生成しません。
Rustではifやmatchは式なので代入することや引数に指定することができます。

fn rust_is_expression_language() {
    let result = 
        if "password!"=="p@ssword!" {
           Ok("success!") 
        } else {
            Err("failed...")
        };
    println!("result = {:?}", match result {
        Ok(s) => s,
        Err(s) => s,
    });
}

上記は何の脈略もないコードですが、if(文?)の結果を変数に代入したり、match式の結果を引数に指定したりできます。

ブロック

Rustのブロックも値を生成するので式です。ブロック内でセミコロン(;)がない場合は、その式の結果がブロックでの戻り値になります。

fn block() {
    let a = {
        "outside block"
    }

    {
        let a = "inside block";
        println!("Where am I? = {}", a);
    }

    println!("Where am I? = {}", a);
}
Where am I? = inside block
Where am I? = outside block

空の文

空の文として、単独のセミコロンが使用できます。

fn empty_statements() {
    let a = 
        if true {
            ;
        };
    println!("a = {:?}", a); // a = ()
}

ブロック内の変数

ブロック内の変数は同じブロック内で宣言した関数内では使用できません

fn block_variable() {
    {
        let a = "outside variable";

        fn inside_function() {
            println!("a = {}", a); // Error!
        }
    }
}

if let式とwhile let式

if let式は以下のように書きます。

if let pattern = expr {
    block1
} else {
    block2
}

意味は、「与えられたexprがpatternにマッチするならblock1が、マッチしなければblock2が実行される」ということだそうです。
つまり順番としては、

  1. exprを評価
    1. の値をpatternでパターンマッチ
      3-1. マッチしたらblock1を実行
      3-2. それ以外ならblock2を実行
fn if_let() {
    fn ret_result() -> Result<String, String> {
        Ok("if_let?".to_string())
    }
    if let Ok(s) = ret_result() {
        println!("s = {}", s); // s = if_let?
    } else {
        println!("error something");
    }
}

while let式は以下のように書きます。

while let pattern = expr {
    block
}

これも、if let式と同じような感じで、「与えられたexprがpatternにマッチすればblockを実行」という意味です。

ループのラベル付け

ループにラベル付けをすることで、任意のタイミングでラベルにジャンプさせることができます。break 'labelcontinue 'labelでジャンプできます。

fn labeled_loop() {
    let vec_a = vec![0, 1, 2, 4, 5];
    let vec_b = vec![0, 1, 2, 3, 4];
    'label:
        for a in &vec_a {
            for b in &vec_b {
                println!("b = {}", b);
                if a!=b {
                    break 'label; // break at a==4, b==3
                }
            }
        }
}

値のないreturn式

値のないreturn式はユニットを返します。

fn return_unit() -> () {
    return
}

また、returnをコメントアウトして戻り値を指定しなくても、ユニットを返します。

エラーハンドリング

panic

panicは異常なエラーが起きた際に発生します。
panic発生時、Rustではスタックを巻き戻すかプロセスをアボート(中断)するかをします。デフォルトではスタックを巻き戻すそうです。
スタックの巻き戻しは、スタックを遡って値をドロップしていきます。
アボートすれば、即座に終了し値のドロップはOSに任せます。

スタックの巻き戻しとダンプ

panicを起こしてみます。

fn panic(v: Vec<i32>) {
    for i in v {
        println!("{}", 10/i);
    }
}

fn main() {
    panic(vec![0, 1, 2, 3]);
}
$ cargo run                                                                                Compiling error v0.1.0 (C:\Users\deta\hack\rust\error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.67s
     Running `target\debug\error.exe`
thread 'main' panicked at 'attempt to divide by zero', src\main.rs:3:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
error: process didn't exit successfully: `target\debug\error.exe` (exit code: 101)

この時、スタックは巻き戻しを行います。
また、環境変数にRUST_BACKTRACE=1を設定すれば、スタックをダンプできます。以下はpowershellの例です。

$ $Env:RUST_BACKTRCE=1
$ cargo run                                                                                 Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target\debug\error.exe`
thread 'main' panicked at 'attempt to divide by zero', src\main.rs:3:24
stack backtrace:
   0: backtrace::backtrace::trace_unsynchronized
             at C:\Users\VssAdministrator\.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.34\src\backtrace\mod.rs:66
   1: std::sys_common::backtrace::_print
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\sys_common\backtrace.rs:47
   2: std::sys_common::backtrace::print
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\sys_common\backtrace.rs:36
   3: std::panicking::default_hook::{{closure}}
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:200
   4: std::panicking::default_hook
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:214
   5: std::panicking::rust_panic_with_hook
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:477
   6: std::panicking::continue_panic_fmt
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:384
   7: std::panicking::rust_begin_panic
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:311
   8: core::panicking::panic_fmt
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libcore\panicking.rs:85
   9: core::panicking::panic
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libcore\panicking.rs:49
  10: error::panic
             at .\src\main.rs:3
  11: error::main
             at .\src\main.rs:8
  12: std::rt::lang_start::{{closure}}<()>
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\src\libstd\rt.rs:64
  13: std::rt::lang_start_internal::{{closure}}
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\rt.rs:49
  14: std::panicking::try::do_call<closure-0,i32>
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:296
  15: panic_unwind::__rust_maybe_catch_panic
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libpanic_unwind\lib.rs:80
  16: std::panicking::try
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panicking.rs:275
  17: std::panic::catch_unwind
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\panic.rs:394
  18: std::rt::lang_start_internal
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\/src\libstd\rt.rs:48
  19: std::rt::lang_start<()>
             at /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54\src\libstd\rt.rs:64
  20: main
  21: invoke_main
             at d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
  22: __scrt_common_main_seh
             at d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
  23: BaseThreadInitThunk
  24: RtlUserThreadStart
error: process didn't exit successfully: `target\debug\error.exe` (exit code: 101)

スタックなので上の例では24が一番古いデータです(たぶん)。11でmainの8行目でエラーが起こっているのが分かります。

アボート

先ほどのpanicをアボートしてみます。

Cargo.toml
...
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"

アボートの設定はCargo.tomlに開発時のコンパイルならprofile.devに、リリース時のコンパイルならprofile.releasepanic = "abort"を追記します。

$ cargo run                                                                                Compiling error v0.1.0 (C:\Users\deta\hack\rust\error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target\debug\error.exe`
thread 'main' panicked at 'attempt to divide by zero', src\main.rs:3:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
error: process didn't exit successfully: `target\debug\error.exe` (exit code: 0xc000001d, STATUS_ILLEGAL_INSTRUCTION)

よく見ると、終了コード(exit code)が101だったのが、16進数の表記になって値も違っています。これはおそらくですが、OS(私の場合はwindows10)での終了コードだと思われます。つまり、アボートすればやはりOSに終了処理を任せているという意味なのではないでしょうか。

panic!

panic!というマクロを使用すれば強制的にその時点でpanicを発生させます。

fn panic_macro() {
    panic!("PANIC!!!");
}
thread 'main' panicked at 'PANIC!!!', .\src\main.rs:8:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

OptionとReust

Rustでは例外の代わりに、戻り値としてOptionまたはResultを使用できます。

なぜ、OptionまたはResultなのか

これは、戻り値にOptionまたはResultを指定することで、呼び出し元はそれらをチェックすることをプログラマに強制させることができます。例外の場合は、それを忘れてしまうと、先に進み予期しない動作を引き起こす可能性があります。
なので、例外よりも安全であるということですね!
以下は、Option型の値をチェックせずにそのまま処理しようとした例です。

fn without_check() {
    let x = 1;
    let y = Some(1);
    let z = x+y;
}
error[E0425]: cannot find function `option_methods` in this scope
  --> .\src\main.rs:32:5
   |
32 |     option_methods();
   |     ^^^^^^^^^^^^^^ not found in this scope

error[E0277]: cannot add `std::option::Option<{integer}>` to `{integer}`
  --> .\src\main.rs:14:14
   |
14 |     let z = x+y;
   |              ^ no implementation for `{integer} + std::option::Option<{integer}>`
   |
   = help: the trait `std::ops::Add<std::option::Option<{integer}>>` is not implemented for `{integer}`

error: aborting due to 2 previous errors

Option

Optionは値が存在するかどうか分からない場合に指定します。存在すればSome(T)を、存在しなければNoneを持ちます。

fn option(s: &str) -> Option<String> {
    if s.to_string().is_empty() {
        None
    } else {
        Some(s.to_string())
    }
}

fn main() {
    println!("option(Some) = {:?}", option("Option!!!"));
    println!("option(None) = {:?}", option(""));
}

全然関係ないのですが、こういう時に全然良い例のコードが思い浮かばないのが悔しいです。笑

Optionの値チェック

Optionの値をチェックするために様々な方法が用意されているようです。

fn option_check() {
    // match
    let x = Some(0);
    let y = match x {
        Some(n) => n,
        None => 1
    };
    println!("y = {}", y); // y = 0
    // is_some(), is_none()
    let mut some: Option<i32> = Some(0);
    let none: Option<i32> = None;
    // is_some, is_none
    println!("some.is_some() = {}", some.is_some()); // some.is_some() = true
    println!("none.is_none() = {}", none.is_none()); // none.is_none() = true
    // expect
    println!("some.expect(\"PANIC!!!\") = {}", some.expect("PANIC!!!")); // some.expect("PANIC!!!") = 0
    // println!("none.expect(\"PANIC!!!\") = {}", none.expect("PANIC!!!")); // panic with message "PANIC!!!"
    // unwrap, unwrap_or, unwrap_or_else
    println!("some.unwrap() = {}", some.unwrap()); // some.unwrap() = 0
    // println!("none.unwrap() = {}", none.unwrap()); // panic
    println!("some.unwrap_or(1) = {}", some.unwrap_or(1)); // some.unwrap_or(1) = 0
    println!("none.unwrap_or(1) = {}", none.unwrap_or(1)); // none.unwrap_or(1) = 1
    fn f() -> i32 {1}
    println!("some.unwrap_or_else(f) = {}", some.unwrap_or_else(f)); // some.unwrap_or_else(f) = 0
    println!("none.unwrap_or_else(f) = {}", none.unwrap_or_else(f)); // none.unwrap_or_elre(f) = 1
    println!("some.unwrap_or_else(|| 1) = {}", some.unwrap_or_else(|| 1)); // some.unwrap_or_else(|| 1) = 0
    println!("none.unwrap_or_else(|| 1) = {}", none.unwrap_or_else(|| 1)); // none.unwrap_or_elre(|| 1) = 1
    // take
    println!("some.take() = {:?}", some.take()); // some.take() = Some(0)
    println!("some        = {:?}", some);        // some        = None
}

他にもドキュメントをみると便利なメソッドが用意されています。「こんなんないかなー」って思ったらドキュメントを見る癖をつけておくことが大事ですね!

Result

Resultは処理が失敗する可能性がある場合に指定します。成功した場合は、Ok(T)を、失敗した場合はErr(T)を持ちます。

fn result(i: i32) -> Result<i32, i32> {
    if i==0 {
        Err(i)
    } else {
        Ok(i)
    }
}

fn main() {
    ...
    println!("result(Ok)  = {:?}", result(1));
    println!("result(Err) = {:?}", result(0));
}
result(Ok)  = Ok(1)
result(Err) = Err(0)

Resultの値チェック

これは、Optionと似ているので省略します。都度、ドキュメントを確認していきたいと思います。

Resultのエイリアス

std::ioなどでは、Result型がよく使うError値とともに定義されているため、Resultのエラーの型を省略できる場合があるようです。ドキュメントでエラーの型が省略されている場合はエイリアスとして定義されているということです。

?演算子

全てのResult値をチェックするために、matchを全て書くのは面倒です。?演算子を使えば呼び出し元へエラーを伝えることができるのでmatchを全て書かなくてすみます。

fn result_reporting() -> Result<usize, usize> {
    let v = vec![0, 1, 2, 3];
    println!("v.binary_search(&3) = {}", v.binary_search(&3)?);
    println!("v.binary_search(&4) = {}", v.binary_search(&4)?);
    Ok(1)
}

fn main() {
    ...
    println!("result_reporting() = {:?}", result_reporting());
}
v.binary_search(&3) = 3
result_reporting() = Err(4)

?演算子はその式が成功すれば値を取り出し、失敗すれば呼び出し元へエラー値を返します。なので、Result型を返す関数内でのみ?演算子が使えるということです。

OptionなのかResultなのか

ここまで来て、ある疑問を抱きました。ある関数において戻り値をOption型にすべきなのか、Result型にすべきなのかはどのように判断すればよいのでしょうか。
これは、私の考えですが、処理が続行可能かどうかで判断すればよいのかなと思っています。
例えば、ファイルが存在するかどうかチェックする関数があり、なければ作成のような処理だった場合は関数の戻り値はOption型だと思います。逆にチェック後に読込みだった場合はResult型にすべきかなと思っています。

ソース

expression
fn rust_is_expression_language() {
    let result = 
        if "password!"=="p@ssword!" {
           Ok("success!") 
        } else {
            Err("failed...")
        };
    println!("result = {:?}", match result {
        Ok(s) => s,
        Err(s) => s,
    });
}

fn block() {
    let a = {
        "outside block"
    };

    {
        let a = "inside block";
        println!("Where am I? = {}", a); // Where am I? = inside block
    };

    println!("Where am I? = {}", a); // Where am I? = outside block
}

fn empty_statements() {
    let a = 
        if true {
            ;
        };
    println!("a = {:?}", a); // a = ()
}

fn block_variable() {
    {
        let a = "outside variable";

        fn inside_function() {
            // println!("a = {}", a); // Error!
        }
    }
}

fn if_let() {
    fn ret_result() -> Result<String, String> {
        Ok("if_let?".to_string())
    }
    if let Ok(s) = ret_result() {
        println!("s = {}", s); // s = if_let?
    } else {
        println!("error something");
    }
}

fn labeled_loop() {
    let vec_a = vec![0, 1, 2, 4, 5];
    let vec_b = vec![0, 1, 2, 3, 4];
    'label:
        for a in &vec_a {
            for b in &vec_b {
                println!("b = {}", b);
                if a!=b {
                    break 'label; // break at a==4, b==3
                }
            }
        }
}

fn return_unit() -> () {
    return
}

fn main() {
    rust_is_expression_language();
    block();
    empty_statements();
    block_variable();
    if_let();
    labeled_loop();
    println!("{:?}", return_unit());
}
result = "failed..."
Where am I? = inside block
Where am I? = outside block
a = ()
s = if_let?
b = 0
b = 1
()
error_handling
fn panic(v: Vec<i32>) {
    for i in v {
        println!("{}", 10/i);
    }
}

fn panic_macro() {
    panic!("PANIC!!!");
}

#[allow(unused)]
fn without_check() {
    let x = 1;
    let y = Some(1);
    // let z = x+y;
}
   
fn option(s: &str) -> Option<String> {
    if s.to_string().is_empty() {
        None
    } else {
        Some(s.to_string())
    }
}

fn option_check() {
    // match
    let x = Some(0);
    let y = match x {
        Some(n) => n,
        None => 1
    };
    println!("y = {}", y); // y = 0
    // is_some(), is_none()
    let mut some: Option<i32> = Some(0);
    let none: Option<i32> = None;
    // is_some, is_none
    println!("some.is_some() = {}", some.is_some()); // some.is_some() = true
    println!("none.is_none() = {}", none.is_none()); // none.is_none() = true
    // expect
    println!("some.expect(\"PANIC!!!\") = {}", some.expect("PANIC!!!")); // some.expect("PANIC!!!") = 0
    // println!("none.expect(\"PANIC!!!\") = {}", none.expect("PANIC!!!")); // panic with message "PANIC!!!"
    // unwrap, unwrap_or, unwrap_or_else
    println!("some.unwrap() = {}", some.unwrap()); // some.unwrap() = 0
    // println!("none.unwrap() = {}", none.unwrap()); // panic
    println!("some.unwrap_or(1) = {}", some.unwrap_or(1)); // some.unwrap_or(1) = 0
    println!("none.unwrap_or(1) = {}", none.unwrap_or(1)); // none.unwrap_or(1) = 1
    fn f() -> i32 {1}
    println!("some.unwrap_or_else(f) = {}", some.unwrap_or_else(f)); // some.unwrap_or_else(f) = 0
    println!("none.unwrap_or_else(f) = {}", none.unwrap_or_else(f)); // none.unwrap_or_elre(f) = 1
    println!("some.unwrap_or_else(|| 1) = {}", some.unwrap_or_else(|| 1)); // some.unwrap_or_else(|| 1) = 0
    println!("none.unwrap_or_else(|| 1) = {}", none.unwrap_or_else(|| 1)); // none.unwrap_or_elre(|| 1) = 1
    // take
    println!("some.take() = {:?}", some.take()); // some.take() = Some(0)
    println!("some        = {:?}", some);        // some        = None
}

fn result(i: i32) -> Result<i32, i32> {
    if i==0 {
        Err(i)
    } else {
        Ok(i)
    }
}

fn result_reporting() -> Result<usize, usize> {
    let v = vec![0, 1, 2, 3];
    println!("v.binary_search(&3) = {}", v.binary_search(&3)?);
    println!("v.binary_search(&4) = {}", v.binary_search(&4)?);
    Ok(1)
}

fn main() {
    // panic(vec![0, 1, 2, 3]);
    // panic_macro();
    println!("option(Some) = {:?}", option("Option!!!"));
    println!("option(None) = {:?}", option(""));
    option_check();
    println!("result(Ok)  = {:?}", result(1));
    println!("result(Err) = {:?}", result(0));
    println!("result_reporting() = {:?}", result_reporting());
}
option(Some) = Some("Option!!!")
option(None) = None
y = 0
some.is_some() = true
none.is_none() = true
some.expect("PANIC!!!") = 0
some.unwrap() = 0
some.unwrap_or(1) = 0
none.unwrap_or(1) = 1
some.unwrap_or_else(f) = 0
none.unwrap_or_else(f) = 1
some.unwrap_or_else(|| 1) = 0
none.unwrap_or_else(|| 1) = 1
some.take() = Some(0)
some        = None
result(Ok)  = Ok(1)
result(Err) = Err(0)
v.binary_search(&3) = 3
result_reporting() = Err(4)

今回はここまで~。
エラーハンドリングって奥深いなーと思いました。こんなときどんなエラー処理をするかを、常に考えていく必要がありそうです。勉強になります。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?