今日の内容
- 「Option」と「Result」を組み合わせたエラーハンドリングの応用
はじめに
前回は、Resultについて学びました。
今回はOptionとResultを組み合わせたエラーハンドリングの応用について学んでいきます。
OptionとResultの相互変換
Rustでは、OptionをResultに変換したり、その逆を行うことができます。
OptionからResultに変換
「値がない(None)」場合にエラーを設定したいときに使います。
fn get_user_id(input: Option<&str>) -> Result<i32, String> {
input
.ok_or_else(|| "値がありません".to_string())
.and_then(|id_str| id_str.parse::<i32>().map_err(|_| "Please type a number!".to_string()))
}
fn main() {
let user_id = get_user_id(Some("42"));
println!("{:?}", user_id);
let invalid_user_id = get_user_id(Some("abc"));
println!("{:?}", invalid_user_id);
let none_user_id = get_user_id(None);
println!("{:?}", none_user_id);
}
/******** 実行結果 ********
Ok(42)
Err("Please type a number!")
Err("値がありません")
*************************/
ResultからOptionに変換
エラー情報が不要な場合は、ok()を使ってResultをOptionに変換します。
fn main() {
let result: Result<i32, &str> = Ok(10);
let option = result.ok();
println!("{:?}", option);
let error_result: Result<i32, &str> = Err("err");
let option = error_result.ok();
println!("{:?}", option);
}
/******** 実行結果 ********
Some(10)
None
*************************/
複数の処理を組み合わせる
以下の例では、ファイルの内容を読み取り、数値を計算する処理を安全に行っています。
use std::fs::File;
use std::io::{self, Read};
fn read_file_content(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn calculate_average(data: &str) -> Result<f64, &'static str> {
let numbers: Result<Vec<f64>, _> = data
.lines()
.map(|line| line.trim().parse::<f64>())
.collect();
let numbers = numbers.map_err(|_| "parse failed!")?;
if numbers.is_empty() {
Err("Data is empty!")
} else {
Ok(numbers.iter().sum::<f64>() / numbers.len() as f64)
}
}
fn main() {
match read_file_content("data.txt") {
Ok(content) => match calculate_average(&content) {
Ok(avg) => println!("average: {:.2}", avg),
Err(e) => println!("Err: {}", e),
},
Err(e) => println!("File error: {}", e),
}
}
- ファイル読み取りのエラーはResultで管理
- 文字列から数値を解析する際もResultでエラーを処理
- matchでネストして、全てのエラーケースに対応
?演算子を使った簡略化
ネストを避けるために、?演算子を活用してコードを簡潔に書き直します。
fn process_file(path: &str) -> Result<f64, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let numbers: Vec<f64> = content
.lines()
.map(|line| line.trim().parse::<f64>())
.collect::<Result<_, _>>()?;
if numbers.is_empty() {
return Err("Data is empty".into());
}
Ok(numbers.iter().sum::<f64>() / numbers.len() as f64)
}
fn main() {
match process_file("data.txt") {
Ok(avg) => println!("avarage: {:.2}", avg),
Err(e) => println!("Err: {}", e),
}
}
- ?を使ってエラーが発生した場合に、即座に関数を終了できる
- ネストを減らし、コードの可読性を向上
OptionとResultの併用例
以下のコードでは、入力を処理し、条件に応じてエラーまたは成功を返します。
fn validate_input(input: Option<&str>) -> Result<i32, &'static str> {
let input = input.ok_or("No input")?;
let number: i32 = input.parse().map_err(|_| "please type number!")?;
if number < 0 {
Err("Negative values not allowed")
} else {
Ok(number)
}
}
fn main() {
let inputs = vec![Some("42"), Some("-10"), None, Some("abc")];
for input in inputs {
match validate_input(input) {
Ok(value) => println!("input value: {}", value),
Err(e) => println!("Err: {}", e),
}
}
}
/******** 実行結果 ********
input value: 42
Err: Negative values not allowed
Err: No input
Err: please type number!
*************************/
複数のエラー型の統合
複数の関数が異なるエラー型を返す場合、統一的に扱うためにBox<dyn std::error::Error>
やthiserror
クレートを利用します。
use std::error::Error;
fn read_file(path: &str) -> Result<String, Box<dyn Error>> {
let content = std::fs::read_to_string(path)?;
Ok(content)
}
fn parse_content(content: &str) -> Result<Vec<i32>, Box<dyn Error>> {
let numbers = content
.lines()
.map(|line| line.parse::<i32>())
.collect::<Result<Vec<_>, _>>()?;
Ok(numbers)
}
fn main() {
let result = (|| -> Result<(), Box<dyn Error>> {
let content = read_file("data.txt")?;
let numbers = parse_content(&content)?;
println!("Data: {:?}", numbers);
Ok(())
})();
if let Err(e) = result {
println!("Err: {}", e);
}
}
おわりに
今回は、OptionとResultを組み合わせたエラーハンドリングの応用について学びました。
次回は、if let について学びたいと思います。
ご精読ありがとうございました。