[C#] ”抜かりのない”自作例外クラスの作り方

例外クラスの自作

C#でエラー処理をする場合、基本的には.NET Frameworkが提供する例外クラスを使用します。しかし、エラーの状況によってはそれらの例外クラスには当てはまるものがない場合がありますよね?そんなときには例外クラスを自作して、それを使うわけですが、独自の例外クラスを作る際は意外に注意点が多いので、今回は”抜かりのない”例外クラスの作り方についてまとめてみました。

「細かいことはいいから手っ取り早く例外クラスの作り方が知りたい!」という方は自作の例外クラスを定義するまで飛んじゃってください。

独自の例外クラスを作る前に

例外クラスを自作する前に、注意しておかなければならない点があります。それは、以下の2点です。

むやみに例外クラスを自作しない

.NET Framework が提供している例外が使える場合は、特別な要件がない限りそれを使いましょう。

例外をエラー処理以外で使わない

例外はエラー処理のための機構です。例外は便利な条件分岐構文ではありません。また、例外を使わなくて済むような状況(TryParse メソッドを使える等の状況)であれば、使わないに越したことはありません。例外処理の実行コストは安くないので、多用しすぎるとパフォーマンスに悪影響を及ぼす可能性があるということを頭に入れておきましょう。マイクロソフト公式解説書である「プログラミング .NET Framework 第4版」には、次のように書かれています。

例外とは、あるメンバーが、その名前から期待される処理を完了できなかった場合のことです。

例外クラスの設計方針

クラス名の末尾は「Exception」で終わるようにする

これは必須ではありませんが、こうすることでそのクラスが例外クラスであることが一目で分かるようになります。

全ての例外クラスは System.Exception から派生させる

C#コンパイラは、System.Exceptionから派生するオブジェクトのみをスローすることができます。※1ちなみに、Exceptionから派生していないオブジェクトをキャッチすることはできます。これは、.NET Framework の CLR(Common Language Runtime) の仕様ではどんなオブジェクトでもスローすることができるからです。他の言語との相互運用をする際のことを考慮した結果、C#は任意のオブジェクトをキャッチすることができるようになっています。

例外クラスの継承階層はなるべく浅くする

例外クラスの継承階層を深くすると、基底型の例外クラスをキャッチするコードは様々なエラーに対処しなければならないことになります。これは望ましくないことです。

さらに悪いことには、例外クラスの継承階層が深いと、新たな派生型の例外クラスを実装する度に、元のソースコード中でその基底となる例外クラスをキャッチしているすべてのコードが正しい動作をするのか検証しなければいけなくなります。

全ての例外クラスはシリアル化可能であるべき

次のような場合、例外クラスをシリアル化する必要があります。

  • アプリケーション実行中にスローされた例外オブジェクトが、アプリケーションドメイン境界を越えるとき
  • 例外をログに出力するとき

自作の例外クラスをそのようなシーンで使用することはないからいらないという場合も多いとは思いますが、シリアル化対応は簡単なので、やっておいたほうがより例外クラスの堅牢性が増すでしょう。

シリアル化とは?

シリアル化とは、オブジェクトをバイトストリームに変換する処理のことです。イメージ的には、オブジェクトの現在の状態をファイルに書き出すような処理をしています。

シリアル化されたオブジェクトは、もはや(.txt などの)普通のファイル同然なので、ディスクに保存してアプリケーションの次回起動のときに読み込んだり、ネットワーク越しに転送することができるようになります。C#では、アプリケーション設定ファイルの保存などでよく使われています。

また、シリアル化されたバイトストリームを元のオブジェクトに戻す処理のことを、逆シリアル化といいます。

シリアル化、アプリケーションドメインについて詳しく知りたい方は、以下のページが参考になります。

シリアル化 (C# ) | Microsoft Docs

アプリケーション ドメイン

自作の例外クラスを定義する

サンプルコード

それでは、上記の例外クラスの設計方針を踏まえて、実際に自作の例外クラスを定義してみましょう。このサンプルコードのクラス名にあたる部分を書き換えれば、それだけで自作例外クラスは完成です。コードについての詳細は後ほど解説します。

using System;
using System.Runtime.Serialization;

[Serializable()] //クラスがシリアル化可能であることを示す属性
public class MyException : Exception
{
    public MyException()
        : base()
    {
    }

    public MyException(string message)
        : base(message)
    {
    }

    public MyException(string message, Exception innerException)
        : base(message, innerException)
    {
    }

    //逆シリアル化コンストラクタ。このクラスの逆シリアル化のために必須。
    //アクセス修飾子をpublicにしないこと!(詳細は後述)
    protected MyException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
}

コードの解説

例外クラスは、System.Exception から派生させます。あとは4つのコンストラクタを定義するだけで、ベーシックな例外クラスの定義は完了です。いずれのコンストラクタも、単純に、与えられたパラメーターを使って基底クラス(ここでは、System.Exception)のコンストラクタを呼び出しているだけです。簡単ですね。

基本の3つのコンストラクタを実装

上から1番目と2番目のコンストラクタは、よく使われるものなのでどういうものかは分かると思います。3番目のコンストラクタは、例外メッセージとともに、その例外の原因となった別の例外への参照を innerException に設定して例外クラスのインスタンスを初期化するためのものです。

例外クラスのシリアル化対応

このコードのキモは、SerializableAttribute 属性と4番目のコンストラクタです。これらによって、例外クラスはアプリケーションドメイン境界を越えることができます。

SerializableAttribute を付けられたクラスは、フォーマッターを使ってシリアル化をすることが可能になります。さらに、例外クラスにSerializableAttribute が付けられている場合、例外がアプリケーションドメイン境界を超えるとき、必要に応じて自動的にシリアル化が行われます。

[Serializable()]
public class MyException : Exception
{...

 

シリアル化され、別のアプリケーションドメインから送出されてきた例外オブジェクトを正しく受け取るためには、それを元の例外オブジェクトに戻す必要があります。この作業を逆シリアル化といいます。

これを行うのが4番目のコンストラクタになります。このコンストラクタは、フォーマッターが呼び出すためにのみ存在しているので、開発者が明示的に呼び出すことはありません。というより、呼び出されると困る※2逆シリアル化コンストラクタが public の場合、任意のコードから不正なデータを渡して例外クラスのインスタンスを作ることができてしまいます。また、この例外クラスを継承禁止にする(sealed 修飾子を指定する)場合には、逆シリアル化コンストラクタに private を指定することも可能です。のでアクセス修飾子は protected に設定します。(フォーマッターからは、アクセス修飾子に関係なく呼び出すことができます。)

protected MyException(SerializationInfo info, StreamingContext context)
    : base(info, context)
{
}

応用編(別記事に続く)

より便利に、また高度に自作例外クラスを使っていくためには以下の手段をとることができます。

これらに関しては別記事(制作中)で書こうと思います。

例外クラスの項目テンプレート化

ここまで見てきたように、例外クラスの自作は高度な作業ではありません。しかし、例外クラスを自作するたびに「あれ?どうやって書けばいいんだっけ?」となるのは良くあることなんじゃないかなと思います。そこで、Visual Studio の項目テンプレートに自作例外クラスのテンプレートを追加しておけば、ボタン一発で自作例外クラスを追加することができます。

「新しい項目の追加」ウィンドウ

追加の情報を保持する例外を自作する

今回作った例外クラスには、例外メッセージと内部例外への参照しか持たせることができません。基本的な例外クラスを作る分にはこれで問題ないです。しかし、自作例外クラスに、例外の原因となった入力値や、エラーコードなどの情報を一緒に持たせたいという場面もあるかと思います。そのような例外クラスを作るには、もう一手間加える必要があります。

脚注   [ + ]

1. ちなみに、Exceptionから派生していないオブジェクトをキャッチすることはできます。これは、.NET Framework の CLR(Common Language Runtime) の仕様ではどんなオブジェクトでもスローすることができるからです。他の言語との相互運用をする際のことを考慮した結果、C#は任意のオブジェクトをキャッチすることができるようになっています。
2. 逆シリアル化コンストラクタが public の場合、任意のコードから不正なデータを渡して例外クラスのインスタンスを作ることができてしまいます。また、この例外クラスを継承禁止にする(sealed 修飾子を指定する)場合には、逆シリアル化コンストラクタに private を指定することも可能です。

コメントを残す

質問・感想などお気軽にどうぞ。
*が付いている項目は入力必須です。メールアドレス以外の項目が公開されます。
スパム防止のため、コメント反映まで少々時間がかかります。