自己紹介
出田 守と申します。
しがない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が実行される」ということだそうです。
つまり順番としては、
- exprを評価
-
- の値をpatternでパターンマッチ
3-1. マッチしたらblock1を実行
3-2. それ以外ならblock2を実行
- の値をpatternでパターンマッチ
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 'label
やcontinue '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をアボートしてみます。
...
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
アボートの設定はCargo.tomlに開発時のコンパイルならprofile.dev
に、リリース時のコンパイルならprofile.release
にpanic = "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型にすべきかなと思っています。
ソース
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
()
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)
今回はここまで~。
エラーハンドリングって奥深いなーと思いました。こんなときどんなエラー処理をするかを、常に考えていく必要がありそうです。勉強になります。