C# Advent Calendar 2015 17日目の記事です。主は来ませり。
#戒めの言葉
コーディングとはコミュニケーションである。
人と実行環境(以下「主」)とのコミュニケーションであり、また、主を介した人と人とのコミュニケーションである。
つまりコーディングとは、読み手と書き手とで成されるコードのキャッチボールであり、キャッチボールであるからには、たとえ例外を投げる場合であっても、それが悪送球であってはならない。
#第一に既存の例外クラス利用を考える
MSDN「スローする正しい種類の例外の選択」には以下のようにあります。
他の既存の例外とは異なる方法でプログラム処理できるエラー条件がある場合は、カスタム例外を作成してスローします。 それ以外の場合は、既存の例外のいずれかをスローします。
チームの例外を所有するという目的のためだけに、新しい例外を作成してスローしないようにします。
俺たちだけのイカした例外を作ろうぜ!ってな目的でカスタム例外を作るのはやめましょう。
逆にカスタム例外が有用であるケースとしては、例えばHttpアクセス機能を持つようなクラスで、特定のステータスコードのときだけ特別なエラー処理を行いたい場合に、WebExceptionをラップするカスタム例外を作る、などが考えられます。
try
{
//do something
}
catch (WebException we)
{
if (we.Status == WebExceptionStatus.ProtocolError
&& (we.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotFound)
{
//カスタム例外
throw new HttpNotFoundException("アクセス先のページが見つかりません", we);
}
//それ以外は再スロー
throw;
}
#業務ルートの分岐を例外で表現しない
Javaでいう検査例外はC#にはありません。
MSDN「例外の推奨事項」にも「通常の使用状態では例外が返されないようにクラスをデザインします」とあるとおり、通常の使用範囲で想定されるルートは例外でなく戻り値を使って表現する、というのが主の思想です。
検査例外の是非を別にしても、例外の投球にはパフォーマンス的にもコストを要するため、不要な例外は生じないように実装すべきです。
文字列を数字に変換できるかどうかわからない場合は、Int32.Parseではなく、検査メソッドであるInt32.TryParseを使うようにします。
#明示的にスローしてはいけない例外
MSDN「標準の例外の種類のキャッチとスロー」にあるとおり、以下のような例外を明示的にスローしてはいけません。
- Exception
- StackOverflowException
- OutOfMemoryException
- ComException
StackOverflowException等は、神の見えざる手によってのみ投げられる例外です。CLRの庭で踊る我々が手を出せる領域ではありません。
また、悪い方向に怠惰になってしまったプログラマなどが、うおりゃと勢いに任せてExceptionを投げているのを見かけることがありますが、絶対にやめましょう。
投げられたExceptionを受け取るためには巨大なグローブが必要になります。そのようなグローブは、掴んではいけない禁断の果実まで掴んでしまいます。
怠惰な人が投げる例外を決めるための指針については後述します。
#NullReferenceException等を明示的にも暗黙的にもスローしない
NullReferenceExceptionは主があなたに向けて投げてくれる球ですので、それをスルーしたり、間違っても自分で投げようとしてはいけません。
Nullを参照することになってしまう異常ケースというのは、そのクラスの実装バグを除けば、そのオブジェクトの使い方がおかしいか、外から渡された引数がNullだったかのどちらかだと思います。
前者であればInvalidOperationExceptionを、後者であればArgumentNullExceptionをスローしましょう。
同様にIndexOutOfRangeExceptionも、インデクサ内の処理からスローする場合を除いて、明示的にも暗黙的にもスローしてはいけません。
範囲外の引数が渡された場合は、自分でArgumentOutOfRangeExceptionをスローしましょう。
private string[] texts;
public string GetText(int index)
{
if (index < 0 || texts.Length <= index)
{
throw new ArgumentOutOfRangeException("index");
}
return texts[index];
}
#明示的に投げる例外の9割はArgumentExceptionかInvalidOperationException
上の例を見てもわかるとおり、自前のメソッドから例外を投げたいケースは、突き詰めると
- メソッドにおかしな引数が渡された
- オブジェクトの現在の状態に対して呼び出せないメソッドが呼び出された
のどちらかに当てはまることがほとんど1です。
このため、例外はArgumentException(+その派生クラス)とInvalidOperationExceptionの2つがあればだいたい事足りることになります。
それ以外の多く(NullReferenceException, DivideByZeroException, InvalidCastException等々)は通常、内部実装の詳細であり、メソッドの呼び出し元に伝える必要がありません。
しいていえば、ObjectDisposedExceptionやFileNotFoundExceptionなどをスローすることで、より具体的な状況を伝えたほうが有益なケースは考えられます。2
public class HogeLoader : IDisposable
{
private bool isDisposed;
public void Dispose()
{
isDisposed = true;
//(リソース破棄等の処理)
}
public Hoge LoadHoge()
{
if (isDisposed)
{
//Disposeしたじゃん!というのを伝える
throw new ObjectDisposedException(this.GetType().FullName);
}
//(Hogeをロードして返す処理)
}
}
#SystemExceptionとApplicationExceptionのことは忘れる
かつて主には、すべてのレイガイをシステム-レイガイとアプリ-レイガイに大別し、システム-レイガイの子を主の投げる例外、アプリ-レイガイの子を人の投げる例外として区別しようという思想がありましたが、なかったことになりました。
過去の資料などでは、カスタム例外を作るときにApplicationExceptionの派生クラスとして作るべしと説明されていることがありますが、現在は何の意味もないのでExceptionの派生クラスとして作りましょうということになっています。
#教え
汝、身の程を知るべし。主を偽るべからず。
ただ主の言葉に耳を傾け、そのライフサイクルを全うせよ。