🚨

RustのResult型:エラー処理を簡単に

2025/03/03に公開

表紙

Rust における Result 型

Rust はシステムプログラミング言語であり、独自のエラー処理メカニズムを提供しています。Rust では、エラーは 2 つのカテゴリに分けられます:回復可能なエラーと回復不可能なエラーです。回復可能なエラーに対して、Rust は Result 型を提供しています。

Result 型の定義

Result 型は列挙型であり、OkErr の 2 つのバリアントを持ちます。Ok バリアントは操作の成功を示し、成功した値を含みます。一方、Err バリアントは操作の失敗を示し、エラーの値を含みます。

以下は Result 型の定義です:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

ここで、T は成功時の値の型、E はエラー時の値の型を表します。

Result 型の用途

Result 型は通常、関数の戻り値として使用されます。関数が正常に実行された場合、Ok バリアントを返し、失敗した場合は Err バリアントを返します。

以下は簡単な例です:

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

この例では、divide 関数が 2 つの引数(分子と分母)を受け取ります。もし分母が 0 の場合、Err バリアントを返し、それ以外は Ok バリアントを返します。

main 関数では divide 関数を呼び出し、match 式を使って戻り値を処理します。Ok バリアントが返ってきた場合は結果を表示し、Err バリアントが返ってきた場合はエラーメッセージを表示します。

Result 型を使用したエラー処理方法

Result 型を返す関数を呼び出す際、発生し得るエラーを適切に処理する必要があります。Rust では、Result 型のエラーを処理するためにいくつかの方法が提供されています。

match 式を使う

match 式は Rust において Result 型のエラーを処理する最も一般的な方法です。戻り値の異なる状況に応じて、異なる処理を実行できます。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

この例では、match 式を使って divide 関数の戻り値を処理し、Ok の場合は結果を表示し、Err の場合はエラーを表示します。

if let を使う

if let 式は match の簡略版であり、特定の 1 つのバリアントにだけ関心がある場合に便利です。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    if let Ok(value) = result {
        println!("Result: {}", value);
    }
}

この例では、Ok バリアントが返ってきた場合のみ結果を表示し、Err バリアントが返ってきた場合は何もしません。

? 演算子を使う

? 演算子を使うと、関数内で発生したエラーを簡単に外部に伝播できます。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn calculate(numerator: f64, denominator: f64) -> Result<f64, String> {
    let result = divide(numerator, denominator)?;
    Ok(result * 2.0)
}

fn main() {
    let result = calculate(4.0, 2.0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

この例では、calculate 関数内で divide 関数を呼び出し、? を使ってエラーを簡単に処理しています。divideErr を返した場合、calculate も即座に Err を返します。

Result 型の便利なメソッド

Result 型には、エラー処理をより簡単にするためのいくつかの便利なメソッドが用意されています。

is_okis_err メソッド

is_ok メソッドは ResultOk であるかを確認し、is_err メソッドは Err であるかを確認します。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    if result.is_ok() {
        println!("Result: {}", result.unwrap());
    } else {
        println!("Error: {}", result.unwrap_err());
    }
}

この例では、is_ok を使って Ok かどうかを判定し、Ok なら unwrap で値を取得し、Err なら unwrap_err でエラーを取得します。

unwrapunwrap_err メソッド

unwrap メソッドは Ok の値を取得し、もし Err だった場合は panic を発生させます。同様に、unwrap_errErr の値を取得し、もし Ok だった場合は panic します。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    let value = result.unwrap(); // エラーが発生すると panic
    println!("Result: {}", value);
}

この例では、unwrap を使って Ok の値を取得しようとしていますが、もし Err が返ってきた場合、panic します。

expectexpect_err メソッド

expectexpect_err メソッドは unwrap の代わりに、エラーメッセージを指定できるメソッドです。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide(4.0, 2.0);
    let value = result.expect("Failed to divide"); // カスタムエラーメッセージ
    println!("Result: {}", value);
}

この例では、もし Err だった場合、"Failed to divide" というカスタムメッセージとともに panic します。

Result 型の特性と利点

Result 型には、以下のような特徴と利点があります:

  • エラーの明示的な処理Result 型を使うことで、エラー処理を明示的に記述する必要があり、エラーを見落とすことが減ります。
  • 型安全Result はジェネリック型であり、成功値とエラー値の型を明確に指定できます。これにより、型の安全性が保証され、誤った型変換を防げます。
  • エラー伝播の容易さ? 演算子を使うことで、エラーを簡単に呼び出し元へ伝播できます。
  • エラーの組み合わせが容易Result 型には andorand_thenor_else などの組み合わせメソッドがあり、複数の Result 型の値を簡単に操作できます。

実際のコードにおける Result の使用

実際のプログラムでは、独自のエラー型を定義し、それを Result 型と組み合わせて使用することが一般的です。

以下に、カスタムエラー型を定義して Result を使用する例を示します:

use std::num::ParseIntError;

type Result<T> = std::result::Result<T, MyError>;

#[derive(Debug)]
enum MyError {
    DivideByZero,
    ParseIntError(ParseIntError),
}

impl From<ParseIntError> for MyError {
    fn from(e: ParseIntError) -> Self {
        MyError::ParseIntError(e)
    }
}

fn divide(numerator: &str, denominator: &str) -> Result<f64> {
    let numerator: f64 = numerator.parse()?;
    let denominator: f64 = denominator.parse()?;
    if denominator == 0.0 {
        Err(MyError::DivideByZero)
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    let result = divide("4", "2");
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(e) => println!("Error: {:?}", e),
    }
}

このコードのポイント:

  • MyError というカスタムエラー型を定義し、ParseIntError も含められるようにしています。
  • From<T> トレイトを実装することで、ParseIntErrorMyError に変換できるようにしています。
  • divide 関数は &str 型の引数を受け取り、それを f64 に変換し、ゼロ除算を検出して適切な Err を返します。

Result を使ったファイル入出力エラーの処理

ファイルの読み書きを行う際、ファイルが存在しない、アクセス権がないなどのエラーが発生することがあります。これらのエラーは Result 型を用いて処理できます。

以下に、ファイルを読み込む関数を Result を使って実装した例を示します:

use std::fs;
use std::io;

fn read_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

fn main() {
    let result = read_file("test.txt");
    match result {
        Ok(content) => println!("File content: {}", content),
        Err(e) => println!("Error: {}", e),
    }
}

コードのポイント:

  • fs::read_to_stringResult<String, io::Error> を返します。
  • read_file 関数は Result<String, io::Error> を返すようにし、エラーが発生した場合はそのまま呼び出し元へ返します。
  • main 関数では match を使ってエラーを処理し、成功時はファイルの内容を表示し、失敗時はエラーメッセージを表示します。

Result を使ったネットワークエラーの処理

ネットワーク通信を行う際、接続タイムアウトやサーバーエラーなど、さまざまなエラーが発生する可能性があります。これらのエラーも Result を使って処理できます。

以下に、TcpStream::connect を用いてサーバーへ接続し、エラーを処理する例を示します:

use std::io;
use std::net::TcpStream;

fn connect(host: &str) -> Result<TcpStream, io::Error> {
    TcpStream::connect(host)
}

fn main() {
    let result = connect("example.com:80");
    match result {
        Ok(stream) => println!("Connected to {}", stream.peer_addr().unwrap()),
        Err(e) => println!("Error: {}", e),
    }
}

コードのポイント:

  • TcpStream::connectResult<TcpStream, io::Error> を返します。
  • connect 関数は Result<TcpStream, io::Error> をそのまま返し、エラーが発生した場合は呼び出し元に伝えます。
  • main 関数では match を使って接続成功時に peer_addr を表示し、失敗時はエラーメッセージを表示します。

Result とエラー処理のベストプラクティス

Rust において Result を適切に使うためには、いくつかのベストプラクティスを守るとよいでしょう:

  1. カスタムエラー型を定義する
    MyError のようなカスタムエラー型を作成することで、エラーの種類を整理しやすくなります。

  2. ? 演算子でエラーを伝播する
    ? を使うことで、エラー処理を簡潔に記述できます。

  3. unwrapexpect の使用を控える
    これらのメソッドは Err の場合に panic を引き起こすため、適切に matchif let を使ってエラーを処理することが推奨されます。

  4. 複数の Result を組み合わせる
    and_thenor_else などのメソッドを使うと、複数の Result 型の値を組み合わせた処理がスムーズになります。

まとめ

Rust の Result 型は、安全で堅牢なエラー処理を実現するための強力なツールです。
この型を適切に活用することで、エラーを明示的に処理し、コードの可読性と保守性を向上させることができます。


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion