続・公式ドキュメントに学ぶ！例外処理のベストプラクティス

前回記事の続きです

👇前回記事

https://qiita.com/sebayashi-tomoya/items/087a52f8b16577002323

[Microsoft公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/standard/exceptions/best-practices-for-exceptions)を参考に例外処理のベストプラクティスを学んでいきます

# 例外スロー時のベストプラクティス
前回は例外を受け取る側のベストプラクティスをまとめましたが、次は例外をスローする際の手法に触れていきましょう💪

## .NET標準の例外クラスを使用すべし
**事前定義の例外クラスが適用されない場合に限り**独自の例外クラスを導入すべきとされています

ということは、.NET標準で定義されている例外以外はなるべく使用しないようにしましょうということですね

考えられる理由としては以下の通り
- 全世界共通で使用されているフレームワークのため、エンジニアごとに認識の相違が起きにくい
- 独自クラス例外はクラスが何を表しているのか解析が必要であり、デバッグが大変
- コーディングに統一性が生まれ、保守性が高くなる

### 標準の例外クラスの代表例
<table border="1">
  <tr>
    <th>例外クラス名</th>
    <th>内容・説明</th>
  </tr>
  <tr>
    <td>ArgumentException</td>
    <td>メソッドに渡された引数が不正な場合にスローされる。</td>
  </tr>
  <tr>
    <td>ArgumentNullException</td>
    <td>引数がnullであってはならない場合に、nullが渡されたときにスローされる。</td>
  </tr>
  <tr>
    <td>ArgumentOutOfRangeException</td>
    <td>引数が許容範囲外の場合にスローされる。</td>
  </tr>
  <tr>
    <td>InvalidOperationException</td>
    <td>オブジェクトの状態が無効な場合に操作が実行されたときにスローされる。</td>
  </tr>
  <tr>
    <td>NullReferenceException</td>
    <td>nullオブジェクトのメンバーにアクセスしようとした場合にスローされる。</td>
  </tr>
  <tr>
    <td>IndexOutOfRangeException</td>
    <td>配列やコレクションのインデックスが範囲外の場合にスローされる。</td>
  </tr>
  <tr>
    <td>FormatException</td>
    <td>値の書式が無効な場合にスローされる。</td>
  </tr>
  </tr>
  <tr>
    <td>NotImplementedException</td>
    <td>呼び出されたメソッドやプロパティが実装されていない場合にスローされる。</td>
  </tr>
  <tr>
    <td>NotSupportedException</td>
    <td>メソッドや操作がサポートされていない場合にスローされる。</td>
  </tr>
  <tr>
    <td>TimeoutException</td>
    <td>操作が指定時間内に完了しなかった場合にスローされる。</td>
  </tr>
  <tr>
    <td>IOException</td>
    <td>入出力エラーが発生した場合にスローされる。</td>
  </tr>
  <tr>
    <td>FileNotFoundException</td>
    <td>指定したファイルが見つからない場合にスローされる。</td>
  </tr>
</table>

### 意図的にスローすべきでない標準例外もある
ただ、AccessViolationException, IndexOutOfRangeException, NullReferenceException, StackOverflowException などは開発者が意図的にスローすべきでないとされています

これらの例外はCLRによって自動的にスローされる例外であって、回復困難なエラーであることが多いからです

詳細は以下に記載されています

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/code-analysis/quality-rules/ca2201

## 例外ビルダーメソッドを使用すべし
様々な場所から同じ例外がスローされるため、例外を作成して返すビルダーメソッドを利用しましょう

実装例(公式ドキュメントより引用)
```c#
class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        // 例外をスローさせる場合はビルダーメソッドを呼び出す
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    /// <summary> FileReaderExceptionを作成して返すビルダーメソッド </summary>
    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
```

こうすることで毎回例外を定義することなく、ルールが統一された例外を作成することができます

## メッセージを適切に設定すべし
例外を生成する際のコンストラクタ引数にメッセージを含めることができます。引数で渡された文字列は`Exception.Message`のプロパティに割り当てられます。

メッセージを決める際には以下の点に注意しましょう
- 多言語での使用が見込まれるアプリケーションでは、例外のメッセージもローカライズした文字列を含めること
- 適切な文法を使用すること

## finally句では例外が発生しないようにすべし
`finally`句で例外が発生すると、アクティブな例外が新しい例外によって隠されてしまいます

それによって、元のエラーの検出およびデバッグが困難になってしまうため、`finally`句では明示的に例外をスローしないようにしましょう

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/code-analysis/quality-rules/ca2219

## 例外をスローすべきでないメソッドを把握すべし
.NETのコーディングにおいて、例外をスローしないことが前提のメソッドが存在します

以下のタイミングでの明示的なスローは避けましょう
- プロパティの get メソッド
- イベント アクセサー メソッド
- Equals メソッド
- GetHashCode メソッド
- ToString メソッド
- 静的コンストラクター
- ファイナライザー
- Dispose メソッド
- 等値演算子
- 暗黙的なキャスト演算子

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/code-analysis/quality-rules/ca1065

## 引数検証例外は同期的にスローすべし
`async`修飾子で宣言されたメソッドでは、スローされた例外は、返される`Task`に格納されるため、タスクが待機されるまで発生しません

非同期メソッドを呼び出すために使用する引数を検証して`ArgumentException`や`ArgumentNullException`をスローする場合は、非同期処理実行前に検証しましょう

https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/exceptions/creating-and-throwing-exceptions#exceptions-in-task-returning-methods

# カスタム例外のベストプラクティス
アプリケーションによっては独自のカスタム例外を実装しなければならないケースもあるでしょう

最後にカスタム例外の実装方法についてまとめていきます

## クラス名の末尾にはExceptionをつけるべし
カスタム例外を実装する場合は、`Exception`クラスやその他適切な例外クラスを継承して新しい例外クラスを新規で作成する必要があります

先ほど.NET標準の例外クラスの代表例をいくつか挙げましたが、全てクラス名の末尾に"Exception"とついてますね

例外クラスであることが一目で判断できるように末尾には"Exception"とつけましょう

## 3つのコンストラクターを含めるべし
独自の例外クラスを実装する際は、他の標準例外クラスと同様の手法で使用できるように以下のコンストラクタを実装しましょう
- 規定の値を使用する`Exception()`
- 文字列のメッセージを受け取る`Exception(String)`
- 文字列のメッセージと内部例外を受け取る`Exception(Stirng, Exception)`

## 必要に応じてプロパティを追加すべし
プロパティを追加した方が例外としての役割を果たせる場合はカスタムメッセージ文字列以外のプロパティを追加するのもありです

例えば、`FileNotFoundException`には見つからないファイル名を提供する`FileName`というプロパティがあります

ただ、プログラミングの点で追加情報が役立つ場合**のみ**追加すべきとされているので、追加するか否かはよく検討しましょう

# 最後に
アプリケーションが落ちなければ良いとただ単にcatchするだけで例外を握りつぶす実装をしていませんか？

正直、私はその場しのぎで例外をとりあえずなんとかする実装をしたことがあります😮‍💨

例外処理を適切に使いこなすことで堅牢なアプリケーションを構築することができます

この記事が例外処理を見つめ直すきっかけになってもらえれば幸いです！