よみとばしポイント
どうも限界派遣SESのnikawamikanです。
ひとりアドカレ18日目の今日もQiitan獲得のために頑張ります。
なお1日遅刻していますが、睡魔には勝てなかったと言っておきます。
先日の記事の続きです。
前回からの続き
前回は基本構文とトレイトについて話しました。
どちらもRustの特徴的な部分で、他の言語とは違う特徴がいくつもあります。
今回はEnumとエラーハンドリングについて話していきます。
Enumとエラーハンドリング
Enumは列挙型とも呼ばれ、複数の値をまとめて扱うための型です。
これ自体は他の言語でもある概念ですが、RustのEnumは他の言語とは違う特徴があります。
よくあるEnum
例えば、Eventごとに処理を分けるようなプログラムを書くときにEnumを使うことがあります。
enum Event {
Start,
Stop,
Pause,
Resume,
}
fn main() {
let event = Event::Start;
match event {
Event::Start => println!("Start"),
Event::Stop => println!("Stop"),
Event::Pause => println!("Pause"),
Event::Resume => println!("Resume"),
}
}
これは他の言語でもよく見られるEnumの使い方です。
Enumに値を持たせる
RustのEnumは他の言語とは違い、Enumに値を持たせることが出来ます。
例えば、Start時に何秒後に処理を開始するかを持たせることが出来ます。
enum Event {
Start(u32),
Stop,
Pause,
Resume,
}
fn main() {
let event = Event::Time(10);
match event {
Event::Start(time) => println!("Start after {} seconds", time),
Event::Stop => println!("Stop"),
Event::Pause => println!("Pause"),
Event::Resume => println!("Resume"),
}
}
このようにEnumに値を持たせることで、Eventごとに値を持たせることが出来ます。
なかなかおもしろいですよね。
これにより、Eventごとに処理を分けるだけでなく、Eventごとに値を持たせることが出来ます。
この機能、他の言語で使いたいなぁー。
エラーハンドリング
上記のEnumの特徴を活かして、RustではエラーハンドリングをEnumを使って行うことが出来ます。
そのEnumがResult
型です。
OkとErrの2つの値を持ち、エラーが発生した場合はErrを返し、正常終了した場合はOkを返します。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
// Errの場合はエラーメッセージを返す
return Err("division by zero".to_string());
}
// Okの場合は計算結果を返す
Ok(a / b)
}
fn main() {
// 0割り算によってエラーが発生する
let result = divide(10, 0);
match result {
Ok(value) => println!("Result: {}", value),
Err(message) => println!("Error: {}", message),
}
}
このように、Result型を使うことでエラーハンドリングを行うことが出来ます。
またエラー時に返す値は任意の型であるため、Enumなどを定義してエラーの種類ごとに処理を分けることも出来ます。
use std::fmt::Display;
enum MyError {
DivisionByZero, // ゼロ除算
NankaError(String), // なんかエラー
}
// Displayトレイトを実装することで、エラーメッセージを表示できるようにする
impl Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MyError::DivisionByZero => write!(f, "division by zero"),
MyError::NankaError(nanka) => {
write!(f, "なんかエラーになったらしいよ 原因 → {}", nanka)
}
}
}
}
fn returns_error() -> Result<(), MyError> {
// なんかエラーを返す
Err(MyError::NankaError("なんかエラーになった".to_string()))
}
fn divide(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
Err(MyError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn main() {
// エラーを返す関数を呼び出す
let result = returns_error();
// エラーが返ってきたら、エラーメッセージを表示する
if let Err(e) = result {
println!("Error: {}", e);
}
// ゼロ除算を行う関数を呼び出す
let result = divide(1, 0);
// エラーが返ってきたら、エラーメッセージを表示する
if let Err(e) = result {
println!("Error: {}", e);
}
}
これで実行すると以下のようになります。
Error: なんかエラーになったらしいよ 原因 → なんかエラーになった
Error: division by zero
いいですよね。
型ごとに決まったエラーを返すことが出来るし、エラーの種類によっては値も一緒に返すことが出来ます。
Nullableな値
RustにはNullという概念はありません。
その代わりにOption型があり、Nullableな値を表すことが出来ます。
Option型はEnumであり、SomeとNoneの2つの値を持ちます。
fn divide(a: i32, b: i32) -> Option<i32> {
// ゼロ除算の場合はNoneを返す
if b == 0 {
return None;
}
Some(a / b)
}
fn main() {
let result = divide(10, 0);
match result {
Some(value) => println!("Result: {}", value),
None => println!("Error: division by zero"),
}
}
このようにOption型を使うことでNullableな値を表すことが出来ます。
なんていうかNullableな型というのを用意するのではなく、Rustの機能であるEnumで表現するのが個人的にすごい刺さっています。
この言語の設計思想のようなものが垣間見えているように感じます。
Option型は早期リターンに使える
Option型は?
演算子と組み合わせることで、早期リターンを行うことが出来ます。
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
return None;
}
Some(a / b)
}
fn all_divide(a: i32, b: i32, c: i32) -> Option<i32> {
let result = divide(a, b)?;
let result = divide(result, c)?;
Some(result)
}
fn main() {
// 最初の割り算で0割り算が発生する
let result = all_divide(10, 0, 5);
match result {
Some(value) => println!("Result: {}", value),
None => println!("Error: division by zero"),
}
}
このように?
演算子を使うことで、早期リターンを行うことが出来ます。
これはエラーハンドリングの際も利用できます。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err("division by zero".to_string());
}
Ok(a / b)
}
fn all_divide(a: i32, b: i32, c: i32) -> Result<i32, String> {
let result = divide(a, b)?;
let result = divide(result, c)?;
Ok(result)
}
fn main() {
let result = all_divide(10, 0, 5);
match result {
Ok(value) => println!("Result: {}", value),
Err(message) => println!("Error: {}", message),
}
}
Rustには例外処理がないため、このように早期リターンを使ってエラーハンドリングを行うことが多いです。
例外処理がある言語では、多くの場合でキャッチするべき例外が多すぎて、どの例外をキャッチするべきか迷ったりしますが、Rustの構文だと何が返ってくるのかが明確なため、エラーハンドリングがしやすいです。
あれ?思ったより書きやすい言語なのでは?
エラーがないことを前提としてunwrap
する
Rustではエラー型を返す関数の中で、エラーがないことを前提として処理を行うことが出来ます。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err("division by zero".to_string());
}
Ok(a / b)
}
fn main() {
let result = divide(10, 5).unwrap(); // エラーがないことを前提としてunwrapする
println!("Result: {}", result);
}
これはunwrap
というメソッドを使うことで、正常終了を前提として値を取り出すことが出来ます。
例えばdivide
関数の第二引数にいれる前に0
が入らないことを保証することが出来る場合、このようにエラーがないことを前提として処理を行うことが出来ます。
しかし、unwrap
はエラーが発生した場合にパニックを起こすため、エラーが発生した際に継続する処理がない場合に使うべきです。
エラーがないことを前提としてexpect
する
unwrap
と似ていますが、expect
はエラーが発生した際のパニックの文章を追記することができます。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err("division by zero".to_string());
}
Ok(a / b)
}
fn main() {
let result = divide(10, 0).expect("エラーが発生しました"); // エラーがないことを前提としてexpectする
println!("Result: {}", result);
}
thread 'main' panicked at src/main.rs:9:32:
エラーが発生しました: "division by zero"
stack backtrace:
0: rust_begin_unwind
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5
1: core::panicking::panic_fmt
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14
2: core::result::unwrap_failed
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1654:5
3: core::result::Result<T,E>::expect
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1034:23
4: bocchi::main
at ./src/main.rs:9:18
5: core::ops::function::FnOnce::call_once
at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/ops/function.rs:250:5
note: Some details are
エラーが発生しました: "division by zero"
となっていますね。
メッセージをカスタムしておけばエラー箇所を特定するのが楽になるかもしれません。
まとめ
Enumとエラーハンドリングについて話しました。
他の言語と違うEnumの使い方ですが、個人的にはとても好きです。
また、今回はコレクションの話は他の言語とあまり差がないと感じたので割愛しています。
ただ、HashMapを利用する際に以下の記事でハッシュ関数を変更できるという話があり参考になったので紹介させてください。
それでは。次回は非同期処理について触れていこうと思います。