【.Net C#】System.Exception を継承すると ISerializable がどうのこうのと警告が出る

厄介なバグに繋がる可能性があるコード

class Test {
  public class Hoge : System.Exception {}
}

問題がある箇所

(1) class Test {
  (2)
  (3) public class Hoge(4) : System.Exception { (5) }
}

(1) public にする。
(2) [Serializable] を指定する。
(3) sealed を付ける。
(4) 名前の末尾に Exception を付ける。
(5) 2種類のコンストラクタが必要(後述)。

修正後のコード

public class Test {
  [Serializable]
  sealed public class HogeException : System.Exception {
    public HogeException() : base() {}
    HogeException(SerializationInfo info, StreamingContext context)
      : base(info, context) {}
  }
}
お分かりいただけただろうか

こんなの知らないと一生気づかないです。
SonarLint の警告で気づくことができました。

System.Exception が ISerializable を継承しているので、カスタムの例外を作る場合は ISerializable のガイドラインに従うことが推奨されます。
といっても、Microsoft 公式の情報はしっちゃかめっちゃかで、把握しづらいです。

なんかよく分かんないので、AI に聞いてみる

Bing AI がそれっぽい回答をくれました。

「例外のシリアライズについて教えてください。」

例外のシリアライズとは、例外オブジェクトをバイトストリームに変換することを指します。これにより、例外オブジェクトを別のプロセスやマシンに転送し、そこで逆シリアル化して再構築することができます。例えば、リモートプロシージャ呼び出し(RPC)などの分散アプリケーションで使用されます。

.NETでは、System.ExceptionクラスはSystem.Runtime.Serialization.ISerializableインターフェースを実装しており、カスタムの例外クラスもこのインターフェースを実装することができます。このインターフェースには、GetObjectDataメソッドが定義されており、このメソッドをオーバーライドして、シリアル化するデータを指定することができます。また、逆シリアル化時には、特殊なコンストラクター(シリアル化コンストラクター)が呼び出されます。このコンストラクターは、SerializationInfoStreamingContextの2つの引数を取ります。

カスタムの例外クラスに独自のデータメンバーがある場合は、それらのメンバーをシリアル化および逆シリアル化するために、上記のメソッドおよびコンストラクターを適切に実装する必要があります。

「C#でカスタム例外を作成する方法はありますか?」

はい、C#でカスタム例外を作成することができます。カスタム例外を作成するには、System.Exceptionクラスを継承し、必要に応じて独自のプロパティやメソッドを追加します。また、基本クラスのコンストラクターをオーバーライドして、独自のコンストラクターを定義することもできます。

以下は、簡単なカスタム例外クラスの例です。
public class MyCustomException : Exception
{
    public MyCustomException()
    {
    }

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

    public MyCustomException(string message, Exception inner)
        : base(message, inner)
    {
    }
}
この例では、MyCustomExceptionクラスがSystem.Exceptionクラスから派生しており、3つのコンストラクターが定義されています。これらのコンストラクターは、基本クラスの対応するコンストラクターを呼び出しています。このようにして、カスタム例外クラスを作成することができます。

↑のように対応しておけば十分なのかも知れませんが、SonarLint のチェックはもっと厳しいです。

  1. [Serializable] は必ず付けなければならない。
  2. 派生クラスがないなら sealed を付けなければならない。
  3. string を引数にとるコンストラクタは使わないなら不要。
  4. string と Exception を引数にとるコンストラクタも同様。
  5. SerializationInfo と StreamingContext を引数にとるコンストラクタが必要。
     ※このコンストラクタは明示的に呼ばないなら private でも構わない。

コンパイラ側でうまいことやって欲しいです。
やることが分かっている&必要な状況も分かっているので、使う側が手間かけないとならないのが面倒。

やだなあ、こわいなぁこわいなぁ

このルールがあるせいで、カスタム例外を気軽に実装できないので、無理やり既存の例外で済ませてしまい、例外がスローされたときに意味が分からなくて調査に時間がかかる…ということが頻発しそう…。

使いそうな例外はどこかにまとめておく…という整備が必要になりそう…。
チームの規模が大きくなるほど、放置すると収集がつかなくなりそう。

逆にシリアライズとデシリアライズを行わないことが明確なら、気にする必要はないということですが、「今は必要ないから対応しないでいいや」…と思って放置しておくと、別のエンジニアの対応で状況が変わり、シリアライズ/デシリアライズのタイミングで原因不明のクラッシュが発生する…ということに繋がりそう。

参考になりそうな記事

公式のドキュメント(英語)

「【.Net C#】System.Exception を継承すると ISerializable がどうのこうのと警告が出る」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です