2011年7月27日水曜日

例外の分類

元ネタは、C#チームのEric Lippert氏のブログの数年前の投稿です。

例外処理というと、「とりあえずキャッチしとけ」とか、逆に「とりあえずキャッチするな(集約例外ハンドラーで対処しろ)」とか言われたりします。かと思えば、「プロなんだから自分の頭で考えろ」とか、「業務フロー次第でしょ」などと千尋の谷に突き落とされたりもします。
極論を言えばそうなのですが、もう少しガイドラインとなるものは無いのか、ということでEricのブログの内容をかいつまんで紹介します。

Ericは、例外は4種類に分類できると言っています[1]

例外の4分類(Eric Lippert氏による)
種類説明特徴対処方法
致命的な例外(fatal exception)
プロセスに深刻な問題が発生し、今にも死にそうな状態にある場合に発生する例外。 プログラマーの過失ではない[2]
復旧は無理(finallyを実行することで悪化する場合も)
何もしない、場合によっては Environment.​FailFast Out​Of​Memory​ExceptionStack​Overflow​ExceptionExecution​Engine​Exceptionなど
うっかり例外(boneheaded exception) プログラマーのうっかりミスにより発生する例外。 プログラマーの過失、
本来、テスト・デバッグの段階ですべて洗い出すべきもの
何もしない(正確に言えばバグとして対処する) Argument​Exception系(後述の頭の痛い例外の場合を除く)、Null​Reference​ExceptionIndex​Out​Of​Range​ExceptionInvalid​Cast​ExceptionDevide​Zero​Exception など。
頭の痛い例外(vexing exception) 失敗する可能性を前提とすべきAPIが、失敗時にスローしてくる例外。
APIの設計ミス
  1. 例外をスローしないAPIを探す(TryParseとかTryGetValue[3]とか)
  2. Try系のメソッドがない場合は例外をキャッチする(もちろん、APIのドキュメントで明示された例外を選択的にキャッチする)
(厄介なメソッドの例)ParseXml​Convert.​VerifyXXXType​Converter.​ConvertXXX [4] など。
外因性の例外(exogenous exception) プログラムの外部の環境に原因のある例外。 避けようがないが、対処しようがある場合もある。 適宜対処する[5] IO​ExceptionSocket​ExceptionWeb​ExceptionExternal​Exception(コードによって例外の種別が異なるので注意、ADO.NET の例外を含む)、Unauthorized​Access​ExceptionSecurity​Exception など。

つまり、処理しなければいけない例外というのは、本来は外因性の例外なわけです。

Windowsなど最近の環境はマルチタスク環境なので、アプリケーションとは別のプロセスがなんらかの処理をしています。ユーザーがうっかりファイルを操作すること だってあり得ます。なので、直前のif文で存在をチェックしたファイルが、その直後にFileStreamを開くまでの間に削除された、なんてことは十分あり得ます。また、ファイルがロックされているかどうかは実際に開いてみるまでわかりませんし(そもそも チェックするAPIがないのですが、あったとしても、if文でチェックした後に他のプロセスがロックする可能性は残ります)、アクセス権も別のプロセス(Explorerとか)が変更するかもしれません。別のマシンにあるプログラムと通信している場合は、障害を起こす可能性のあるものが途中に多数介在しますので、基本的にエラーが起こるものとして考えないといけません。

何かお役にたてば。

[1] InterruptionExceptionCancellationExceptionなど、この4つに入らないものもあります。

[2] CLRやJITの実装の隙間を探るようなコードを書くような方については、もちろんその人の過失。

[3] ただし、IDictionary<TKey,TValue>のインデクサが例外をスローするのは、値の型が値型の場合(たとえばInt32)に、値として0が格納されているのか、要素がなかったからdefault(Int32)(= 0)を返したのかを区別できないので、設計ミスではないと思います。
参照型とNullable<T>をいっしょくたにしたジェネリック型制約がかけられないジェネリックの設計ミスとも言えなくもありませんが、IDictionary<string, int?>としなければならないといったルールが追加され、APIの敷居が上がってしまうように思います。

[4] CanConvertXXXメソッドもあるのですが、おそらくは同じであろうロジックを2回呼び出すというのは設計として微妙だと思っています。WPFのIValueConverterは値を変換できない場合にDependencyProperty.UnsetValueを返すというコントラクトでいい感じですね。

[5] 結局千尋の谷に突き落としていますが、ここだけを考えれば良いということでどうかご容赦を。

2011年7月5日火曜日

スローする例外チートシート

.NETには様々な例外があります。
どんな例外があるのかについては、某書籍 なり、ufcppさんのサイトなりを見ていただくとして、ここではどんなときにどんな例外をスローするかを知ってる範囲で挙げていこうと思います。あと、ここでは主にライブラリとかフレームワークとかミドルウェアとか、そのレイヤーの観点で紹介します。アプリケーションは対象外です(赤間さんのblogとかを見ると良いかと)

私見が入っていますが、以下のようにして選ぶといいと思います。

  1. これ以上続行すると、データ破壊など副作用が起こる。
    • Environment.FailFast
  2. プログラマーにミスはないが、プログラム的に対処できるエラーが発生した。
    • 既存の例外で対処できる
      •  ディクショナリ(やそれに似たデータ構造)で指定したキーに対応する値がない:KeyNotFoundException
      • 必要なファイルがない:FileNotFoundException
      • など(挙げるときりがないので省略)
    • 既存の例外では対処できない
      • System.Exceptionから派生した独自例外を作る
  3. 引数がそれ単独でおかしい。
    • nullにできない(または、その引数がnullであることに意味がない。たとえば、必須の引数):ArgumentNullException
    • 引数が範囲外(たとえばインデックスが負):ArgumentOutOfRangeException
    • 列挙型の引数が、未定義の数値(.NETの場合、列挙型で定義していない数値を列挙型にキャストして渡せるので):InvalidEnumArgumentException、またはArgumentOutOfRangeException
      • 実装時点では未知の列挙値に対しては、NotSupportedExceptionというのもあり
    • Parseメソッド(やそれに準ずるメソッド)で、引数が形式に合わない(TryParseで本来falseを返すような値):FormatException
    • パスがそのプラットフォームでの最大長を超えている:PathTooLongException
    • その他:ArgumentException
      • Messageプロパティに具体的な理由を入れましょう。
  4. APIの使い方がおかしい
    • そもそもDisposeした後に呼び出すとかおかしいよ:ObjectDisposedException
    • その他、引数はあっているけど、呼び出し方がおかしい場合(プロパティの値が矛盾している、必須のプロパティが設定されていないなど):InvalidOperationException
  5. 何らかの理由で、ある操作を仕様として実行できない
    • 具体的には(ランタイムより下にある)プラットフォーム(まぁOS)の都合で実行できない:PlatformNotSupportedException
    • その他(指定した列挙値をサポートしていない、継承したメンバーをサポートできない(読み取り専用のコレクションやストリームでの書き込み処理など)):NotSupportedException
  6. ごめん、まだ実装してない(テストファーストだとか、互換ライブラリを作っていて今回のリリースでは未実装とか)
    • NotImplementedException
  7. それ以外
    • 足しますのでご一報ください。
何かのお役にたてば。