第4章 トレイトとエラーハンドリング
Rust には他の言語に見られるクラス継承の仕組みがありません。
代わりに、インターフェースに相当する「トレイト(Trait)」と、強力なジェネリクスを組み合わせて柔軟なポリモーフィズムを実現します。また、例外機構(try-catch)を持たず、エラーを値として扱う安全なエラーハンドリングモデルを採用しています。
本章では、トレイトの基本、主要な標準トレイト、そして Option や Result を使った堅牢なエラー処理を学びます。
4.1 トレイトの基本とジェネリクス
トレイトは、複数の型に共通して実装させたいメソッドの仕様(インターフェース)を定義する仕組みです。
トレイトの定義と実装
#![allow(unused)]
fn main() {
// トレイトの定義
pub trait Summary {
fn summarize(&self) -> String;
}
// 構造体の定義
pub struct NewsArticle {
pub headline: String,
pub author: String,
}
// 構造体へのトレイトの実装
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{} by {}", self.headline, self.author)
}
}
}
トレイト境界(Trait Bounds)
ジェネリクス関数において、特定のトレイトを実装している型のみを受け取るように制約を設けることができます。
#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item: &T) {
println!("速報! {}", item.summarize());
}
}
上記の notify 関数は、Summary トレイトを実装している型であれば、どのような構造体でも安全に引数として受け取ることができます。
4.2 派生トレイト(Derive)と標準トレイト
Rust では、頻繁に利用する標準的なトレイトを #[derive(...)] 属性を付与するだけで自動生成(自動実装)できます。
#[derive(Debug, Clone, PartialEq)]
pub struct Point {
pub x: i32,
pub y: i32,
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1.clone(); // Clone トレイトにより複製可能
// Debug トレイトにより {:?} で中身を出力可能
println!("デバッグ出力: {:?}", p1);
// PartialEq トレイトにより == で比較可能
if p1 == p2 {
println!("p1 と p2 は同一の座標です");
}
}
ユーザー向けに整形した文字列を表示したい場合は、標準ライブラリの std::fmt::Display トレイトを手動で実装します。
4.3 エラーハンドリングの思想
Rust ではエラーを「回復不能エラー」と「回復可能エラー」の 2 種類に明確に分類します。
回復不能エラー(panic!)
配列の範囲外アクセスや深刻な内部不整合など、プログラムを直ちに停止させるべき重大な欠陥が発生した場合は panic! マクロが呼び出されます。
#![allow(unused)]
fn main() {
fn crash_now() {
panic!("致命的な問題が発生しました。プログラムを終了します。");
}
}
回復可能エラー
ファイルが見つからない、ネットワーク接続がタイムアウトしたなど、想定されるエラーはプログラムを落とさず、適切な代替処理やユーザーへの通知を行うべきです。Rust では、これらを「値」として関数の戻り値で表現します。
4.4 Option 型と Result 型の実践
値の有無を表す Option 型
Rust には他の言語のような null や nil が存在しません。値が存在するか空であるかは Option<T> 型で厳格に表します。
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T), // 値が存在する
None, // 値が存在しない
}
}
値を取り出すには、パターンマッチを行って必ず None の場合を考慮しなければならないため、NullPointerException のような実行時クラッシュが原理的に防がれます。
成否を表す Result 型
処理が成功したか失敗したかを表すには Result<T, E> 型を使用します。
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T), // 成功し、型 T の結果を含む
Err(E), // 失敗し、型 E のエラー情報を含む
}
}
? 演算子によるスマートなエラー伝播
エラーハンドリングを簡潔に行うために、Rust では ? 演算子が用意されています。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // 失敗した場合は直ちに Err を return する
let mut username = String::new();
file.read_to_string(&mut username)?; // 失敗した場合は直ちに Err を return する
Ok(username) // 成功した場合は Ok で包んで返す
}
}
? 演算子を変数の直後に付けると、Ok の場合は中身の値をそのまま取り出し、Err の場合は現在の関数からその Err を即座にリターンします。これにより、エラーハンドリングコードでネストが深くなるのを防ぎ、処理の本質を読みやすく保つことができます。