🦀
Rustの勉強 9日目
The Rust Programming Language 日本語版 第9章
エラー処理の話。Goと同様に回復可能エラーと回復不能エラーがある。
9.1 panic!で回復不能なエラー
-
panic!
マクロを呼ぶとソースコードで呼ばれた箇所を指摘して巻き戻る -
Cargo.toml
で[profile.release]
のpanic = 'abort'
を指定すると、巻き戻しから異常終了になる -
RUST_BACKTRACE=1
とするとバックトレースが取れる
9.2 Resultで回復可能なエラー
- Result型という列挙型を使ってエラー処理をするのが慣例
- Goだったら
t, err
のタプルで受け取っているのをResult型でやっている
- Goだったら
- マッチガードを使うときにガード条件式にムーブされてほしくない場合には
ref
キーワードを使う -
unwrap
とexpect
を使うと、エラーの処理を省略して、エラーだった場合にはパニックするようにしてくれる-
expect
の場合はエラーメッセージが選べる
-
- エラーを移譲したかったらそのままエラーを返したらいい
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
を返す関数でしか使えない
- ただし
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)
}
9.3 panic!すべきかするまいか
- だいたい
Result
を返したほうがいい
Rustlings
errors1.rs
テストでは Result
を扱うように書かれていたんで generate_nametag_text
の戻り値を変更
pub fn generate_nametag_text(name: String) -> Result<String, String> {
if name.is_empty() {
Err(String::from("`name` was empty; it must be nonempty."))
} else {
Ok(format!("Hi! My name is {}", name))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_nametag_text_for_a_nonempty_name() {
assert_eq!(
generate_nametag_text("Beyoncé".into()),
Ok("Hi! My name is Beyoncé".into())
);
}
#[test]
fn explains_why_generating_nametag_text_fails() {
assert_eq!(
generate_nametag_text("".into()),
// Don't change this line
Err("`name` was empty; it must be nonempty.".into())
);
}
}
errors2.rs
item_quantity.parse::<i32>()
が Result
を返すので、その下の Ok
の型が合わない。なので、Err
だった場合にはそのままエラーを返すように ?
を追加したらテストが通った。
use std::num::ParseIntError;
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;
let qty = item_quantity.parse::<i32>()?;
Ok(qty * cost_per_item + processing_fee)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn item_quantity_is_a_valid_number() {
assert_eq!(total_cost("34"), Ok(171));
}
#[test]
fn item_quantity_is_an_invalid_number() {
assert_eq!(
total_cost("beep boop").unwrap_err().to_string(),
"invalid digit found in string"
);
}
}
errors3.rs
main
は ()
を返す関数なので、中で ?
は使えない。なので match
を使ってエラーハンドリングした。
use std::num::ParseIntError;
fn main() {
let mut tokens = 100;
let pretend_user_input = "8";
let cost = total_cost(pretend_user_input);
let cost = match cost {
Ok(c) => c,
Err(e) => panic!("Unexpected input: {}", e),
};
if cost > tokens {
println!("You can't afford that many!");
} else {
tokens -= cost;
println!("You now have {} tokens.", tokens);
}
}
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;
let qty = item_quantity.parse::<i32>()?;
Ok(qty * cost_per_item + processing_fee)
}
errors4.rs
value
が非正値のときの処理がなかったので追加
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
if value > 0 {
Ok(PositiveNonzeroInteger(value as u64))
} else if value == 0 {
Err(CreationError::Zero)
} else {
Err(CreationError::Negative)
}
}
}
#[test]
fn test_creation() {
assert!(PositiveNonzeroInteger::new(10).is_ok());
assert_eq!(
Err(CreationError::Negative),
PositiveNonzeroInteger::new(-10)
);
assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0));
}
errors5.rs
「まだ教えてないBoxとTraitが出てくるけど、分かると思うよー」って書いてあったけど、わからないんで先にドキュメントの方チラ見しておく。
12章のあたりにトレイトオブジェクトを返してエラー型をまとめて返せるようにするみたいな説明があったんだけれども、雰囲気でしかわかってない。ただこれでテストは通った。
ここで use std::num::ParseIntError
が明に使われてないのに必要になってるのは ?
の中で暗に使われているからだろうか
use std::error;
use std::fmt;
use std::num::ParseIntError;
// TODO: update the return type of `main()` to make this compile.
fn main() -> Result<(), Box<dyn error::Error>> {
let pretend_user_input = "42";
let x: i64 = pretend_user_input.parse()?;
println!("output={:?}", PositiveNonzeroInteger::new(x)?);
Ok(())
}
// Don't change anything below this line.
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
x if x == 0 => Err(CreationError::Zero),
x => Ok(PositiveNonzeroInteger(x as u64)),
}
}
}
// This is required so that `CreationError` can implement `error::Error`.
impl fmt::Display for CreationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let description = match *self {
CreationError::Negative => "number is negative",
CreationError::Zero => "number is zero",
};
f.write_str(description)
}
}
errors6.rs
unwrap
で正しくエラーを返していなかったのを、型を合わせるようにした
use std::num::ParseIntError;
// This is a custom error type that we will be using in `parse_pos_nonzero()`.
#[derive(PartialEq, Debug)]
enum ParsePosNonzeroError {
Creation(CreationError),
ParseInt(ParseIntError),
}
impl ParsePosNonzeroError {
fn from_creation(err: CreationError) -> ParsePosNonzeroError {
ParsePosNonzeroError::Creation(err)
}
fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError {
ParsePosNonzeroError::ParseInt(err)
}
}
fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> {
let x = s.parse();
match x {
Err(e) => Err(ParsePosNonzeroError::from_parseint(e)),
Ok(i) => PositiveNonzeroInteger::new(i).map_err(ParsePosNonzeroError::from_creation),
}
}
// Don't change anything below this line.
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
x if x == 0 => Err(CreationError::Zero),
x => Ok(PositiveNonzeroInteger(x as u64)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_error() {
// We can't construct a ParseIntError, so we have to pattern match.
assert!(matches!(
parse_pos_nonzero("not a number"),
Err(ParsePosNonzeroError::ParseInt(_))
));
}
#[test]
fn test_negative() {
assert_eq!(
parse_pos_nonzero("-555"),
Err(ParsePosNonzeroError::Creation(CreationError::Negative))
);
}
#[test]
fn test_zero() {
assert_eq!(
parse_pos_nonzero("0"),
Err(ParsePosNonzeroError::Creation(CreationError::Zero))
);
}
#[test]
fn test_positive() {
let x = PositiveNonzeroInteger::new(42);
assert!(x.is_ok());
assert_eq!(parse_pos_nonzero("42"), Ok(x.unwrap()));
}
}
Discussion