エラー処理を行う
ここでunwrap()について考えてみましょう。
このコードの中でunwrap()が使われているのは下記の3箇所です。
enable_raw_mode().unwrap();
let input_byte = input_byte.unwrap();
disable_raw_mode().unwrap();
enable_raw_mode()のドキュメントを見ると、enable_raw_mode()はResultという型を返す関数になっています。他の2箇所も同様です。つまりunwrap()はResultのメソッドを呼び出していることになります。
では、Resultとは何でしょうか!?こちらもドキュメントに書いてあります。
Resultは列挙型で成功を示すOk()か失敗を示すErr()のどちらかがセットされる型になります。他の言語では例外処理をtry文等で書いたりしますがRustではこのResultを使ってエラー処理を行います。
unwrap()はこのResultを処理するための一つの方法ですが他にも様々な処理の仕方があります。どのような方法があるのか少し見てみましょう。
様々なエラー処理を試してみる
input_byteの処理コードを切り出してそれぞれのResult処理を試してみましょう。
unwrap()
unwrap()はResultがOk()であればその値を取り出し、Err()であればパニックして終了します。
もし回復可能なエラーであれば終了してしまうのは問題があります。unwrap_or()等、パニックを起こさないメソッドもありますので別の方法を検討すべきでしょう。
#![allow(unused)]
use std::io::Error;
fn main() {
let input_byte: std::result::Result<&str, Error> = Ok("This is ok");
let input_byte = input_byte.unwrap();
println!("{}", input_byte);
let input_byte: std::result::Result<&str, Error> = Err(Error::other("This is Error"));
let input_byte = input_byte.unwrap();
println!("{}", input_byte);
println!("End of main()");
}
expect()
Err時にメッセージを追加できるというところがunwrap()との大きな違いでしょうか。それ以外はunwrap()と同じくパニックしてしまうので別の方法を検討すべきでしょう。
#![allow(unused)]
use std::io::Error;
fn main() {
let input_byte: std::result::Result<&str, Error> = Ok("This is ok");
let input_byte = input_byte.expect("This is expect");
println!("{}", input_byte);
let input_byte: std::result::Result<&str, Error> = Err(Error::other("This is Error"));
let input_byte = input_byte.expect("This is expect");
println!("{}", input_byte);
println!("End of main()");
}
match式
他の言語ではswitch文が同等になると思います。
この例ではinput_byte1がOkの場合、Errの場合、それ以外の場合(アンダーバー)をそれぞれ記述しています。
match式の特徴としてはとりうる全てのパターンを網羅しなければならない、ということです。ResultはOkかErrのいずれかの値になるので、match式にOkだけの記述をしてもコンパイルエラーとなります。
アンダーバーはワイルドカードのようなもので全てにマッチします2。例えばinput_byte の型がResultではなくu8型だった場合、u8がとりうる数字の範囲は0-255になります。256パターンも記述していたら大変ですので、例えば値が1の場合のみ成功パターンとして扱い、他の値の場合はエラーメッセージを出したいとすれば、1 => success(), _ => println!("Error!")
のようになるでしょう。
またmatchは式なので返り値を取得できます。この例ではそれぞれのパターンからの返り値Stringをinput_byteに再び入れています。
#![allow(unused)]
use std::io::Error;
fn main() {
let input_byte: std::result::Result<&str, Error> = Ok("This is ok");
let input_byte: String = match input_byte {
Ok(value) => value.to_string(),
Err(value) => value.to_string(),
_ => "This is Unexpected".to_string(),
};
println!("{}", input_byte);
let input_byte: std::result::Result<&str, Error> = Err(Error::other("This is Error"));
let input_byte: String = match input_byte {
Ok(value) => value.to_string(),
Err(value) => value.to_string(),
_ => "This is Unexpected".to_string(),
};
println!("{}", input_byte);
println!("End of main()");
}
?演算子
Okの処理だけをmatch式で記述したい場合、全てのパターンを網羅しないといけない制約はコードが冗長になってしまいます。
Rustでは?演算子が用意されており、Okの値を取り出すことができるショートカットが用意されています。Err処理は移譲されます。そのため、Resultを返すメソッドや関数内でしか?演算子は使えません。
#![allow(unused)]
use std::io::Error;
fn main() -> Result<(), Error> {
let input_byte: std::result::Result<&str, Error> = Ok("This is ok");
let input_byte = input_byte?;
println!("{}", input_byte);
let input_byte: std::result::Result<&str, Error> = Err(Error::other("This is Error"));
let input_byte = input_byte?;
println!("{}", input_byte);
println!("End of main()");
Ok(())
}
if let 式
より簡潔に記述したい場合は、if let 式がよいでしょう。この例では、Okの値をvalueにキャプチャできた場合、成功パターンを記述しています。Errを処理したいケースも同様に記述可能です。
この式は?演算子よりも制約がなく使いやすいです。
この例では、match式と同様に返り値をinput_byteに入れようとしていますが、else文を書かない場合、返り値として()が返されてしまいStringとマッチしません。そのため、Stringを返すようにelseを書いています。
#![allow(unused)]
use std::io::Error;
fn main() {
let input_byte: std::result::Result<&str, Error> = Ok("This is ok");
let input_byte: String = if let Ok(value) = input_byte {
value.to_string()
} else {
"This is Unexpected".to_string()
};
println!("{}", input_byte);
let input_byte: std::result::Result<&str, Error> = Err(Error::other("This is Error"));
let input_byte: String = if let Err(value) = input_byte {
value.to_string()
} else {
"This is Unexpected".to_string()
};
println!("{}", input_byte);
println!("End of main()");
}
マクロ
println!()に付いても説明していませんでした。
単語の最後に!マークがつく文はマクロと呼ばれるものです。また#[derive]などもマクロの一種です。
println!マクロの使い方は今まで見てきた通り関数と同じです。大きな違いはマクロは引数を可変にすることができるということでしょうか。
ということでprintln!で16進数を表示するように改良してみましょう。
といっても難しくはありません。Hex: {0:#X}
を足すだけです。
println!マクロの基本的な使い方はプレースホルダーと同じ数の変数を並べる使い方ですが、
プレースホルダーに引数の番号を指定すると同じ数でなくても出力することができます(この例ではコロンの左が引数 0番)。3
match式も追加したコードは下記のようになります。
// error_handling
// 標準ライブラリの読み込み
use std::io::{self, Read};
// crosstermの読み込み
use crossterm::terminal::disable_raw_mode;
use crossterm::terminal::enable_raw_mode;
// main 関数
fn main() {
// raw モードに移行
enable_raw_mode().unwrap();
// STDINからbyte列を読み込み
for input_byte in io::stdin().bytes() {
// match式でResultを処理
match input_byte {
// 正常ケース
Ok(input_byte) => {
// 読み込んだbyteをcharに変換して束縛
let output_char = input_byte as char;
// 読み込んだ値を出力
if output_char.is_control() {
println!("Hex: {0:#X} Binary: {0:08b} ASCII: {0:#03} \r", input_byte);
} else {
println!(
"Hex: {0:#X} Binary: {0:08b} ASCII: {0:#03} Character: {1:#?}\r",
input_byte, output_char,
);
// qが入力されたらbreak
if output_char == 'q' {
// raw モードを解除
disable_raw_mode().unwrap();
break;
}
}
}
// エラーの場合
Err(err) => println!("Error: {}", err),
}
}
}
- シャドーイングを駆使しすぎてわかりづらいコードになっています。良くない例です。 ↩︎
- アンダーバーの評価をOkやErrの評価の前に持ってくるとアンダーバーの評価だけになりますので注意。 ↩︎
- 書式についてはこちら。 ↩︎
TOC
- 準備
- ターミナル入出力を試す
- ターミナル系crateを利用する
- 編集データと表示処理
- 画面制御
- その他
GitHubにコードをアップロードしています。
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --examples first_step