はじめに
こんにちは。細々とプログラミングをしているsotanengelです。
この記事は以下の記事の連載です。
他の連載記事 (詳細)
- Day 1:型システムを使ってデータ構造を再現しよう
- Day 2:型システムを用いて共通の挙動を表現しよう
- Day 3:OptionとResultに対してはmatchを用いずに変換しよう
- Day 4:標準のErrorを使おう
- Day 5:型変換を理解しよう
- Day 6:newtypeパターンを活用しよう
- Day 7:複雑な型にはビルダを使おう
- Day 8:明示的なループの代わりにイテレータ変換を使用することを検討しよう
- Day 9:標準トレイトに習熟しよう
- Day 10:RIIパターンにはDropトレイトを実装しよう
- Day 11:ジェネリクスとトレイトオブジェクトのトレードオフを理解しよう
- Day 12:デフォルト実装を用いて、実装しなければならないトレイトメソッドを最小限にしよう
- Day 13:Don't panic
- Day 14:リフレクションを避けよう
- Day 15:可視範囲を最小化しよう
- Day 16:ワイルドカードインポートを避けよう
- Day 17:パブリックインターフェースのドキュメントを書こう
- Day 18:分別を持ってマクロを使おう
- Day 19:Clippyに耳を傾けよう
- Day 20:ユニットテスト以上のものを書こう
また本記事はEffective Rust(David Drysdale (著), 中田 秀基 (翻訳))を参考に作成されております。とてもいい書籍ですので興味を持った方は、ぜひ読んでみてください!
今日の内容
概要
RustのError
トレイトを独自のエラー型に実装することで、呼び出し側に適切にエラー情報を伝えられるようにしよう。
String情報を持つ独自のエラーを実装しよう
問(リンク)
String
をError
としてそのまま使うことはできないため、独自の型(OriginalError
)を定義しましょう。
コード (詳細)
// TODO: StringはErrorを実装していないため、formatが使えるように独自の型を定義し、Errorを実装してください。
fn do_something(flag: bool) -> Result<(), String> {
if flag {
Ok(())
} else {
"Something went wrong".to_string()
}
}
fn main() {
match do_something(false) {
Ok(_) => println!("Success!"),
Err(e) => {
let error_message = format!("Failed: {}", e);
println!("{}", error_message);
}
}
}
解答(リンク)
From
トレイトも実装することで、String
からの変換も容易にすることができます。
コード (詳細)
use std::error;
use std::fmt;
// 独自エラー型の定義 (タプル構造体)
#[derive(Debug)]
pub struct OriginalError(String);
// Displayトレイトを実装してエラーメッセージを整える
impl fmt::Display for OriginalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error: {}", self.0)
}
}
// error::Errorトレイトを実装
impl error::Error for OriginalError {}
// From<String> を実装して String からの変換を簡単に
impl From<String> for OriginalError {
fn from(message: String) -> Self {
OriginalError(message)
}
}
// &str からの変換もできるようにする
impl From<&str> for OriginalError {
fn from(message: &str) -> Self {
OriginalError(message.to_string())
}
}
fn do_something(flag: bool) -> Result<(), OriginalError> {
if flag {
Ok(())
} else {
Err(OriginalError::from("Something went wrong"))
}
}
fn main() {
match do_something(false) {
Ok(_) => println!("Success!"),
Err(e) => {
let error_message = format!("Failed: {}", e);
println!("{}", error_message);
}
}
}
エラーをネストしよう
問(リンク)
error::Error
トレイトでsource
メソッドをAppError
に実装することで?
でエラーを伝播できるようにしましょう。
コード (詳細)
use std::error;
use std::fmt;
use std::io;
use std::string::FromUtf8Error;
#[derive(Debug)]
pub enum AppError {
NotFound(io::Error),
InvalidInput(FromUtf8Error),
InternalError(Box<dyn error::Error>),
}
// Displayトレイトを実装してエラーメッセージを整える
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(e) => write!(f, "Not Found: {}", e),
AppError::InvalidInput(e) => write!(f, "Invalid Input: {}", e),
AppError::InternalError(e) => write!(f, "Internal Error: {}", e),
}
}
}
// TODO: sourceメソッドを記載して、processの処理で?でエラーを伝播できるようにしてください。
impl error::Error for AppError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {}
}
// From<String> を実装して String からの変換を簡単に
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::NotFound(err)
}
}
impl From<FromUtf8Error> for AppError {
fn from(err: FromUtf8Error) -> Self {
AppError::InvalidInput(err)
}
}
impl From<String> for AppError {
fn from(message: String) -> Self {
AppError::InternalError(Box::new(io::Error::new(io::ErrorKind::Other, message)))
}
}
// サンプル関数
fn do_something(input: &str) -> Result<(), AppError> {
if input.is_empty() {
Err(io::Error::new(
io::ErrorKind::NotFound,
"Input cannot be empty",
))?
} else if input == "404" {
Err(AppError::from("Requested resource not found".to_string()))?
} else if input == "500" {
Err(io::Error::new(
io::ErrorKind::Other,
"Unexpected error occurred",
))?
}
Ok(())
}
// プロセス関数をResultでラップ
fn process() -> Result<(), AppError> {
let inputs = vec!["", "404", "success"];
for input in inputs {
do_something(input)?; // エラーがあれば即座にreturn
println!("Processed: {}", input);
}
Ok(())
}
fn main() {}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_do_something_not_found() {
let result = do_something("");
match result {
Err(AppError::NotFound(e)) => {
assert_eq!(e.kind(), io::ErrorKind::NotFound);
assert_eq!(e.to_string(), "Input cannot be empty");
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_do_something_internal_error() {
let result = do_something("404");
match result {
Err(AppError::InternalError(e)) => {
assert!(e.to_string().contains("Requested resource not found"));
}
_ => panic!("Expected InternalError"),
}
}
#[test]
fn test_do_something_success() {
let result = do_something("success");
assert!(result.is_ok());
}
#[test]
fn test_process() {
let result = process();
match result {
Err(AppError::NotFound(e)) => {
assert_eq!(e.to_string(), "Input cannot be empty");
}
Err(AppError::InternalError(e)) => {
assert!(e.to_string().contains("Requested resource not found"));
}
Ok(_) => {} // 成功ケースは特にアサートしない
_ => panic!("Unexpected error"),
}
}
#[test]
fn test_from_io_error() {
let io_err = io::Error::new(io::ErrorKind::Other, "io error");
let app_error: AppError = io_err.into();
match app_error {
AppError::NotFound(e) => {
assert_eq!(e.to_string(), "io error");
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_from_utf8_error() {
let utf8_err = String::from_utf8(vec![0, 159]).unwrap_err();
let app_error: AppError = AppError::from(utf8_err);
match app_error {
AppError::InvalidInput(e) => {
assert!(e.to_string().contains("invalid utf-8"));
}
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_from_string() {
let app_error: AppError = String::from("custom error").into();
match app_error {
AppError::InternalError(e) => {
assert_eq!(e.to_string(), "custom error");
}
_ => panic!("Expected InternalError"),
}
}
}
解答(リンク)
コード参照。
コード (詳細)
use std::error;
use std::fmt;
use std::io;
use std::string::FromUtf8Error;
#[derive(Debug)]
pub enum AppError {
NotFound(io::Error),
InvalidInput(FromUtf8Error),
InternalError(Box<dyn error::Error>),
}
// Displayトレイトを実装してエラーメッセージを整える
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(e) => write!(f, "Not Found: {}", e),
AppError::InvalidInput(e) => write!(f, "Invalid Input: {}", e),
AppError::InternalError(e) => write!(f, "Internal Error: {}", e),
}
}
}
// Errorトレイトを実装
impl error::Error for AppError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
AppError::NotFound(e) => Some(e),
AppError::InvalidInput(e) => Some(e),
AppError::InternalError(e) => Some(e.as_ref()),
}
}
}
// From<String> を実装して String からの変換を簡単に
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::NotFound(err)
}
}
impl From<FromUtf8Error> for AppError {
fn from(err: FromUtf8Error) -> Self {
AppError::InvalidInput(err)
}
}
impl From<String> for AppError {
fn from(message: String) -> Self {
AppError::InternalError(Box::new(io::Error::new(io::ErrorKind::Other, message)))
}
}
// サンプル関数
fn do_something(input: &str) -> Result<(), AppError> {
if input.is_empty() {
Err(io::Error::new(
io::ErrorKind::NotFound,
"Input cannot be empty",
))?
} else if input == "404" {
Err(AppError::from("Requested resource not found".to_string()))?
} else if input == "500" {
Err(io::Error::new(
io::ErrorKind::Other,
"Unexpected error occurred",
))?
}
Ok(())
}
// プロセス関数をResultでラップ
fn process() -> Result<(), AppError> {
let inputs = vec!["", "404", "success"];
for input in inputs {
do_something(input)?; // エラーがあれば即座にreturn
println!("Processed: {}", input);
}
Ok(())
}
fn main() {}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_do_something_not_found() {
let result = do_something("");
match result {
Err(AppError::NotFound(e)) => {
assert_eq!(e.kind(), io::ErrorKind::NotFound);
assert_eq!(e.to_string(), "Input cannot be empty");
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_do_something_internal_error() {
let result = do_something("404");
match result {
Err(AppError::InternalError(e)) => {
assert!(e.to_string().contains("Requested resource not found"));
}
_ => panic!("Expected InternalError"),
}
}
#[test]
fn test_do_something_success() {
let result = do_something("success");
assert!(result.is_ok());
}
#[test]
fn test_process() {
let result = process();
match result {
Err(AppError::NotFound(e)) => {
assert_eq!(e.to_string(), "Input cannot be empty");
}
Err(AppError::InternalError(e)) => {
assert!(e.to_string().contains("Requested resource not found"));
}
Ok(_) => {} // 成功ケースは特にアサートしない
_ => panic!("Unexpected error"),
}
}
#[test]
fn test_from_io_error() {
let io_err = io::Error::new(io::ErrorKind::Other, "io error");
let app_error: AppError = io_err.into();
match app_error {
AppError::NotFound(e) => {
assert_eq!(e.to_string(), "io error");
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_from_utf8_error() {
let utf8_err = String::from_utf8(vec![0, 159]).unwrap_err();
let app_error: AppError = AppError::from(utf8_err);
match app_error {
AppError::InvalidInput(e) => {
assert!(e.to_string().contains("invalid utf-8"));
}
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_from_string() {
let app_error: AppError = String::from("custom error").into();
match app_error {
AppError::InternalError(e) => {
assert_eq!(e.to_string(), "custom error");
}
_ => panic!("Expected InternalError"),
}
}
}
さいごに
もしも本リポジトリで不備などあれば、リポジトリのissueやPRなどでご指摘いただければと思います。