十分に調節された例外

Eric Gunnerson
Microsoft Corporation

August 28, 2001

サンプル コードのダウンロード MSDN Code Center からサンプル (exception.exe) をダウンロードする

数ヶ月前、私はコードを記述していたところ、 例外クラスが予想どおりに機能しないことに気づきました。 私は例外をキャッチして、その例外を同様の種類の例外にラップしたいと考えました。 しかし、例外を受け取るコンストラクタがなかったので、 この処理を行うことができませんでした。 つまり、クラス デザイナが (string message,Exception inner) コンストラクタを含んでいなかったのです。

このことから、私は例外クラスをどのように記述する必要があるかについて考えました。 そして、私はどんな規則があるかを正確に理解していなかったことに気付きました。 少し調査して、少しコードを記述してみたところ、 例外クラスを記述する適切な方法を見つけ出しました。 さらに、例外クラスを記述する場合、間違いを犯しやすいと判断しました。

そこで、今月は例外クラスのデザインについてお話して、 例外クラスのデザインを評価するユーティリティをご紹介することにしました。

新しい例外が必要ですか ?

.NET Framework には、開発者がコードで使用できる汎用の例外が多数あります。 たとえば、引数が Null 値を保持していることを調べる場合、 ArgumentNullException をスローできます。 既存の例外があまり適切でない場合は、 独自の例外クラスを記述する必要があります。

基本的な例外

新しい例外を作成することを決めた場合、 その例外に名前を付ける必要があります。 例外がスローされた理由がわかる名前を考えるようにしてください。

すべての例外クラスは "Exception" という単語で終わり、 ApplicationException クラスから派生する必要があります。 これは厳密な必要条件ではないので、 この条件に従わなくても機能しないわけではありません。 ただし、この必要条件に従うとほかの人があなたの記述したコードを保守することがより簡単になります。 例外クラスの基本的なアウトラインは次のようになります。


public class MyException: ApplicationException
{
}

この例外の実用的なインスタンスを作成するには、 例外を構築する方法が必要になります。 標準なデザイン パターンでは、すべての例外は 3 つの基本的なコンストラクタを持つ必要があります。 以下にその 3 つのコンストラクタを示します。


public MyException()
public MyException(string message)
public MyException(string message, Exception inner)

最初の 2 つのコンストラクタを使用して、スローされる例外を作成します。 多くの場合、2 番目のコンストラクタが使用されます。

3 番目のコンストラクタを使用して、 例外を詳細情報と共にラップします。 大部分の例外クラスでは、 これら 3 つのコンストラクタは単にパラメータを基本クラスのコンストラクタに渡すだけです。 たとえば、3 つ目のコンストラクタは次のようにコーディングします。


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

例外クラスを使い始めるには、これで十分でしょう。 実際、このようなクラスは多くの状況で使用できます。 ただし、あるコンピュータから別のコンピュータにこの例外を転送したい場合があるかもしれませんが、 現在のバージョンはこの用途では機能しません。 さらに悪いことには、このような状況で例外をスローしても、 ランタイムがその例外を別のシステムに送ろうとしない限り、 問題が表面化しないのです。

したがって、たとえ例外がこのような状況で使用されることがないと思っても、 すべての例外クラスをシリアル化および逆シリアル化できるようにしておくことは重要です。 これを行うために、時間もコードもそれ程必要なく、 コードのデバッグが困難になることもないでしょう。

例外のシリアル化

大部分のクラスでは、 [Serializable] 属性をクラスに追加するだけで、 シリアル化できるようになり。 ただし、この場合例外クラスは ISerializable インターフェイスを実装するので、 オブジェクトを作成するために "逆シリアル化コンストラクタ" を定義します。 (逆シリアル化コンストラクタは、 以前にシリアル化されたデータからオブジェクトを作成するために呼び出されます。) このクラスは Exception から派生するので、 このようなコンストラクタを複製する必要があります。

このサンプルの例外にはフィールドがないので、 ただ呼び出しを基本クラスのコンストラクタに渡すことは安全です。


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

逆シリアル化コンストラクタを記述すると、 汎用の例外クラスの作成は完了します。 サンプル ファイル ExcSimple.cs は次のクラスを含んでいます。


// 単純な例外クラスのサンプル ExcSimple.cs
//
// この例外クラスは、例外が適切な方法で処理
// されるために必要な処理をすべて行います。

using System;
using System.Runtime.Serialization;

[Serializable]
public class MySimpleException: ApplicationException
{
      // 基本的な 3 つのコンストラクタ
   public MySimpleException()
   {
   }

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

   public MySimpleException(string message, Exception inner): 
base(message, inner)
   {
   }

      // 逆シリアル化コンストラクタ
   public MySimpleException(SerializationInfo info, 
                               StreamingContext context):
         base(info, context)
   {
   }
}

補足的なフィールドの追加

テキスト形式のメッセージだけでは十分でなく、 例外にさらにデータを追加したい場合があるでしょう。 たとえば、問題を発生させたパラメータの名前をキャプチャしたい場合があるとします。

この操作を行うには、例外クラスにもう少しコードを追加する必要があります。 まず、フィールドとフィールドにアクセスするためのプロパティを追加します。 このサンプルでは、フィールドの値を取得するために、 "value" という名前の整数型のフィールドを追加し、 読み取り専用のプロパティを追加します。


int value;
public int Value
{
   get
   {
      return value;
   }
}

次に、この新しい値を受け取るコンストラクタを追加します。 追加するコンストラクタの数は、 追加した値がどのように使用されるかによって異なります。 このサンプルでは、メッセージとその値を受け取る 1 つのコンストラクタを追加することにしました。


public MyException(string message, int value): base(message)
{
   this.value = value;
}

さらに、新しいフィールドをシリアル化するようにクラスを設定する必要があります。 このクラスを ISerializable インターフェイスに実装しているクラスとしてマークし、 GetObjectData() メソッドを実装します。


public override void GetObjectData(SerializationInfo info,
                                   StreamingContext context)
{
   base.GetObjectData(info, context);
   info.AddValue("Value", value);      
}

厳密に言えば、このクラスは既に基本クラスから実装を継承しているので、 このクラスが ISerializble インターフェイスを "再実装している" クラスとしてマークされるということです。 この関数は、最初に基本クラスの関数を呼び出して、クラスのデータを確実に保存し、 その後その値を保存するようにします。 文字列 "Value" はキーとして渡され、 保存したフィールドに関連付けられます。 したがって、後で値を取り出すために同じキーを使用できます。

**注意   ** GetObjectData() という名前は少しわかりにくいかもしれません。 簡単に言うと、この関数はオブジェクトのデータを取得するためにシリアライザによって呼び出されます。 そこで、この名前が付いています。

次に、逆シリアル化コンストラクタがその値を取り出せるように、 逆シリアル化コンストラクタを修正する必要があります。 ここでは、単純にそのキーの値を取り出すだけです。


public MyException(SerializationInfo info, StreamingContext context):
      base(info, context)
{
   value = info.GetInt32("Value");
}

これら両方の場合に、基本クラスがフィールドを保存、または復元できるようにする必要があります。 そうしないと、クラスは正常にシリアル化も逆シリアル化もできません。

最後に、Message プロパティに追加したフィールドの値を含めるように、 このプロパティをオーバーライドする必要があります。 つまり、このサンプルの例外で ToString() を呼び出すと、 基本クラスのメッセージと追加したフィールドの情報の両方が表示されます。 以下にプロパティの実装を示します。


public override string Message
{
   get
   {
         // 注意: テキストをローカライズする必要があります
      string s = String.Format("Value: {0}", value);
      return base.Message + Environment.NewLine + s;
   }
}

フィールドが参照型であれば、null の場合の処理を行う必要があるでしょう。 しかし、整数型の場合にはその必要がなく、 単にメッセージに整数値を追加するだけです。 また、実際の例外クラスでは文字列をローカライズする必要があることに注意してください。

すべて完成すると、最終的には次のようなクラスになります。 これは ExcField.cs で確認できます。


// 追加フィールドのある例外クラスのサンプル ExcField.cs 
//
// この例外クラスは、例外が適切な方法で処理
// されるために必要な処理をすべて行います。

using System;
using System.Runtime.Serialization;

[Serializable]
public class MyException: ApplicationException, ISerializable
{
   int value;

      // 基本的な 3 つのコンストラクタ
   public MyException()
   {
   }
   public MyException(string message): base(message)
   {
   }
   public MyException(string message, Exception inner): 
base(message, inner)
   {
   }
      // 追加された値を受け取るコンストラクタ
   public MyException(string message, int value): base(message)
   {
      this.value = value;
   }
      // 逆シリアル化コンストラクタ
   public MyException(SerializationInfo info, 
                         StreamingContext context):
         base(info, context)
   {
      value = info.GetInt32("Value");
   }

   public int Value
   {
      get
      {
         return value;
      }
   }

      // オブジェクトからデータを取得するために
      // シリアル化中にフレームワークから呼び出されます。
   public override void GetObjectData(
         SerializationInfo info,
         StreamingContext context)
   {
      base.GetObjectData(info, context);
      info.AddValue("Value", value);      
   }

      // オーバーライドされた Message プロパティ。
      // このプロパティは、追加したフィールドの値と共に
      // 例外をテキスト形式で適切に示します。
   public override string Message
   {
      get
      {
            // 注意: ローカライズする必要があります
         string s = String.Format("Value: {0}", value);
         return base.Message + Environment.NewLine + s;
      }
   }
}

簡単なテスト アプリケーション

2 つの例外クラスに加えて、 これらのクラスがシリアル化および逆シリアル化できることを確認する SerializeTest.cs という名前の短いテスト プログラムを含めました。 test.bat を実行して、クラスをビルドし、確認ツールを実行します。

例外チェッカ

この記事を書いているとき、 例外クラスが何をする必要があるかついて混乱しやすいことに気付きました。 適切な処理をより簡単に行うために (また、私が気が付いたことを示すために)、 ExceptionSurvey という名前のプログラムを作成しました。

このユーティリティは使用中の例外クラスが正しく記述されていることを確認します。 このユーティリティはディレクトリにあるすべてのアセンブリを開き、 すべての例外クラスを検索します。 さらに、クラスごとに以下のことを確認します。

  1. 例外の名前は "Exception" という単語で終わります。

  2. 例外は 3 つの基本的なコンストラクタを実装します。

    
    class();
    class(string message);
    class(string message, Exception inner);
    
  3. 例外には [Serializable] 属性が設定されます。

  4. 例外は逆シリアル化コンストラクタを実装します。

    
    class(SerializationInfo info, StreamingContext context)
    
  5. 例外は public フィールドを持っていません。

  6. 例外が private フィールドを持っている場合、GetObjectData() を実装します。

  7. 例外が private フィールドを持っている場合、Message プロパティをオーバーライドします。

このユーティリティは例外クラスを記述しているときに発生する問題の大部分を解決してくれるでしょう。 このユーティリティを使用するには、 アセンブリが存在するディレクトリに移動して、 プログラムを実行するだけです。 このユーティリティは、検出したすべての問題のレポートを作成します。 test.bat を実行すると、チェッカもビルドされて実行されます。

チェッカがどのように機能するかという説明はこのコラムの対象範囲外ですが、 このコラムのソースにはたくさんのコメントが付いていて、 操作があまり複雑ではないことがおわかりいただけると思います。

Web サイトの寄せ集め

今月、ご紹介するサイトは 1 つだけです。

Eric Gunnerson は、C# コンパイラ チームの QA 担当の責任者で、C# デザイン チームのメンバでもあり、『A Programmer's Introduction to C#』の著者でもあります。 彼は、8 インチのフロッピー ディスクが何であるかを知っているぐらい昔からプログラミングを行っています。さらに、かつては簡単にテープを装着できました。