Rust 初心者です。
見よう見まねで Rust プログラミングを約一か月続けてみました。
これまで見てきた中でなかなか印象深かった、複数種類のエラー型を一つの文脈で扱う方法を自分なりにまとめてみます。
お題
例えばある Rust プログラムではエラーナンバーを保持するようなエラー型 MyErrNumber を使っているとします。これを「MyErrNumber 文化圏」と呼ぶことにします。
// MyErrNumber はエラーナンバーを保持する型 (std::error::Error を実装)
#[derive(Debug)]
pub struct MyErrNumber {
number: i32,
}
impl MyErrNumber {
pub fn new(number:i32) -> Self {
//(・・・省略・・・)
impl std::fmt::Display for MyErrNumber {
//(・・・省略・・・)
impl std::error::Error for MyErrNumber {
//(・・・省略・・・)
// returns_MyErrNumber はエラー時に MyErrNumber を返す関数
fn returns_MyErrNumber() -> Result<(), MyErrNumber> {
Err(MyErrNumber::new(666))
}
他方、別のプログラムではエラーメッセージを保持するエラー型 MyErrMessage を使っているとします。これを「MyErrMessage文化圏」と呼ぶことにします。
// MeErrMessage はエラーメッセージを保持する型 (std::error::Error を実装)
#[derive(Debug)]
pub struct MyErrMessage {
message: String,
}
impl MyErrMessage {
pub fn new(message:&str) -> Self {
//(・・・省略・・・)
impl std::fmt::Display for MyErrMessage {
//(・・・省略・・・)
impl std::error::Error for MyErrMessage {
//(・・・省略・・・)
// returns_MyErrMessage はエラー時に MyErrMessage を返す関数
fn returns_MyErrMessage() -> Result<(), MyErrMessage> {
Err(MyErrMessage::new("Some error!"))
}
ここで、MyErrNumber 文化圏の関数と MyErrMessage 文化圏の関数を、同じ一つの関数から使いたい。
その際に、「?」を使ってエラーを返すにはどうすればよいか?
例えばこんなことをやりたい↓
// MyErrNumber 文化圏と MyErrMessage 文化圏が混在
fn foo (flag:bool) -> Result<(), XXXX>
if flag {
returns_MyErrNumber()?;
} else {
returns_MyErrMessage()?;
}
Ok(())
}
(1) 統合するエラー型の利用
MyErrNumber と MyErrMessage を統合するエラー型 MyErr を作る。
// MyErr は MyErrNumber と MyErrMessage を統合する型 (std::error::Errorを実装)
#[derive(Debug)]
pub enum MyErr {
MyErrNumber(MyErrNumber),
MyErrMessage(MyErrMessage),
}
ここで、From トレイトを実装して MyErrNumber と MyErrMessage から変換できるようにしておく。
// MyErrNumber からの変換
impl From<MyErrNumber> for MyErr {
fn from(err_number:MyErrNumber) -> Self {
MyErr::MyErrNumber(err_number)
}
}
// MyErrMessage からの変換
impl From<MyErrMessage> for MyErr {
fn from(err_message:MyErrMessage) -> Self {
MyErr::MyErrMessage(err_message)
}
}
//(・・・省略・・・)
この新しいエラー型 MyErr を使って、お題の foo 関数の名前を変えて次のように与えます。
// MyErrNumber 文化圏と MyErrMessage 文化圏が混在
fn returns_MyErr (flag: bool) -> Result<(), MyErr>
if flag {
// Err<MyErrNumber> が返されるが Err<MyErr> に変換して返される
returns_MyErrNumber()?;
} else {
// Err<MyErrMessage> が返されるが Err<MyErr> に変換して返される
returns_MyErrMessage()?;
}
Ok(())
}
(2) std:: error::Error を利用
MyErrNumber も MyErrMessage もいずれも std:: error::Error トレイトを実装していることを利用します。
お題の foo 関数の名前を変更して次のように与えます。
// MyErrNumber 文化圏と MyErrMessage 文化圏が混在
fn returns_dyn_Error(flag:bool) -> Result<(), Box<dyn std::error::Error>> {
if flag {
// Err<MyErrNumber> が返されるが std::error::Error を実装するインスタンスとして扱われる
returns_MyErrNumber()?;
} else {
// Err<MyErrMessage> が返されるが std::error::Error を実装するインスタンスとして扱われる
returns_MyErrMessage()?;
}
Ok(())
}
(1) と (2) の結果
次のようなコードで (1) と (2) の結果をみてみます。
fn main() {
// MyErrNumber のエラーが返る
if let Err(e) = returns_MyErrNumber() {
let typ = type_of(&e);
println!("returns_MyErrNumber():\t{:?} type={}", e, typ)
}
// MyErrMessage のエラーが返る
if let Err(e) = returns_MyErrMessage() {
let typ = type_of(&e);
println!("returns_MyErrMEssage():\t{:?} type={}", e, typ)
}
// returns_MyErr(true) は MyErrNumber を MyErr に変換したエラーが返るはず
if let Err(e) = returns_MyErr(true) {
let typ = type_of(&e);
println!("returns_MyErr(true):\t{:?} type=\"{}\"", e, typ)
}
// returns_MyErr(false) は MyErrMessage を MyErr に変換したエラーが返るはず
if let Err(e) = returns_MyErr(false) {
let typ = type_of(&e);
println!("returns_MyErr(false):\t{:?} type=\"{}\"", e, typ)
}
// returns_dyn_Error(true) は MyErrNumber のエラーが返るはず
if let Err(e) = returns_dyn_Error(true) {
let typ = type_of(&e);
println!("returns_dyn_err(true):\t{:?} type=\"{}\"", e, typ)
}
// returns_dyn_Error(false) は MyErrMessage のエラーが返るはず
if let Err(e) = returns_dyn_Error(false) {
let typ = type_of(&e);
println!("returns_dyn_err(true):\t{:?} type=\"{}\"", e, typ)
}
}
// type_of は型を調べる関数。杜甫々氏に敬礼!
// https://www.tohoho-web.com/ex/rust.html#typeof
fn type_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
動作結果です。
returns_MyErrNumber(): MyErrNumber { number: 666 } type=&study0219::err::MyErrNumber
returns_MyErrMEssage(): MyErrMessage { message: "Some error!" } type=&study0219::err::MyErrMessage
returns_MyErr(true): MyErrNumber(MyErrNumber { number: 666 }) type="&study0219::err::MyErr"
returns_MyErr(false): MyErrMessage(MyErrMessage { message: "Some error!" }) type="&study0219::err::MyErr"
returns_dyn_err(true): MyErrNumber { number: 666 } type="&alloc::boxed::Box<dyn core::error::Error>"
returns_dyn_err(true): MyErrMessage { message: "Some error!" } type="&alloc::boxed::Box<dyn core::error::Error>"
期待通りの結果となりました。
が、(2) の returns_dyn_err の出力で返り値の型は、どちらも "&alloc::boxed::Box<dyn core::error::Error>" となり型的には見分けはつかないことがわかりました。
(2) でどのエラー型のインスタンスが返ってきたのか判別するには、上のようなデバッグ出力 "{:?}" で得られた文字列をチェックするくらいしかなさそうです。
まとめ
複数のエラー型を一つの文脈で混在して使うようなケースの対処方法をまとめてみました。
(1) と (2) のプログラミング上のメリット/デメリットを以下の通り検討してみました。
(1) のメリット
- エラーの種類を一つのエラー型でまとめて可視化可能である。
- エラーの種類に応じた対応処理の分岐が可能である。
(1) のデメリット
- 統合するエラー型を自分で用意する必要がある。
- どんなエラーの種類があるのか前もって把握する必要がある
(2) のメリット
- std:: error::Error トレイトさえ実装しているなら任意のエラー型に対応できる。
- どんなエラーの種類があるのか前もって把握する必要がない。
- (1) のように新しいエラー型を用意する必要がない。
(2) のデメリット
- エラーを受け取った側では std:: error::Error を実装したインスタンスであることしかわからない。
- エラーの種類に応じた対応処理の分岐ができない。(ちゃんと言うと型に基づいた分岐はできないが、文字列化すればできるかもしれない。でも面倒くさそう)
感覚的に、エラーが起きたことさえわかればいいなら (2)、エラーの種類に応じてきめこまかな対応を行いたいのなら (1) という感じでしょうか。
せいぜい一か月程度のプログラミング経験でしかありませんので、他の観点、パフォーマンス面やセキュリティ面での差異についてはまだまださっぱりわかりません。
最後に。thiserror を使うと…
(1) のデメリットを補うものとして、thiserror を使ってコード量を低減することができるそうなので使ってみました。
thiserror によるコード例。
use thiserror::Error;
// MyErr は MyErrNumber と MyErrMessage を統合する型 (std::error::Errorを実装)
#[derive(Error, Debug)]
enum MyErr {
#[error("MyErrNumber({0})")]
MyErrNumber(#[from] MyErrNumber),
#[error("MyErrMessage({0})")]
MyErrMessage(#[from] MyErrMessage),
}
std::fmt::Display と From の実装を簡単に行うことができます。
コード量が減ってコーディングミスも少なくなりそうだし、これはもう標準クレートにしちゃってよいのではないですかね。
以上です。