実行時に何かがうまくいかないと(ファイルがない、テキストが数値でない、キーが辞書にない)、.NETは例外を投げます。例外はエラーを表すオブジェクトです。例外は catch ブロックに処理されるまで呼び出し履歴をさかのぼります。どこでも処理されなければ、プログラムは停止してエラーを表示します。
基本的なtry catch
失敗するかもしれないコードを try で囲み、失敗を catch で処理します。
出力:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
int.Parse("forty-two") が例外を投げると、try ブロックの中のその後の Console.WriteLine は飛ばされ、catch (FormatException) ブロックが実行され、ループは続きます。例外オブジェクトが不要なら、変数は省略できます(catch (FormatException))。
この例には、もっと適した手段があります。int.TryParse(input, out int n) は例外を投げる代わりに false を返します。例外は、コードが想定していない状況のためのものです。不正なことがよくある入力は想定内なので、キャッチするのではなく確認します。
例外の伝わり方
呼び出しの連鎖の深いところで投げられた例外は、一致するハンドラーが見つかるまで、すべてのメソッドを巻き戻します。それらの各メソッドで、例外が投げられた後のコードは実行されません。
出力:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf と OrderTotal には catch がないので、例外はそれらを素通りして Main に届きます。ハンドラーは、その失敗に対して何をすべきかを知っているレベルに置きます。それは多くの場合、失敗が起きた場所ではありません。
特定の例外を正しい順序でキャッチする
catch 句は、その型と、そこから派生したすべての型を処理します。句が複数ある場合は最初に一致したものが採用されるので、最も限定的なものから最も一般的なものへと並べます。
出力:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
最後の呼び出しは OverflowException を投げ、これはどちらの限定的な句でも処理されないので、一般的な catch (Exception e) が処理します。catch (Exception) を最初に置くと、その後の句が到達できなくなり、コンパイラはエラーCS0160でそれを拒否します。
whenによる例外フィルター
C# 6で when が追加されました。catch 句を適用するかどうかを決める条件です。false なら、ランタイムはその句が存在しないかのように、別のハンドラーを探し続けます。
出力:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
フィルターを使えば、キャッチして再スローすることなく、例外の中のデータで分岐できます。3つ目の句のように、関係のない2つの型を同じように処理するためのすっきりした方法でもあります。フィルターはスタックが巻き戻される前に実行されるので、どのフィルターも一致しない場合、デバッガーやクラッシュダンプには元の状態がそのまま残ります。
finally:必ず実行されるコード
finally ブロックは、制御が try から離れるとき、つまり最後まで終わったとき、早めにreturnしたとき、例外を投げたときのいずれでも実行されます。後片付けを書く場所です。(1つ注意点があります。例外をどこでもキャッチしない場合、プロセスは finally を実行せずに終わることがあります。)
出力:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
どの場合も、メソッドの結果が Main に届く前に「Close connection」が表示されます。finally は、return の値が計算された後、メソッドが実際に戻る前に実行されます。try には catch なしで finally だけを付けることもでき、その場合は後片付けをしながら例外を呼び出し元へ伝えます。
IDisposable を実装するオブジェクト(ファイル、ストリーム、接続)では、using文がこの try/finally を書いてくれます。
再スロー:throw;とthrow e;
catch ブロックで何かを記録してから、例外をそのまま先へ進めたいことがあります。どう再スローするかで、スタックトレースが残るかどうかが決まります。
出力:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; は例外を catch ブロックから新しく投げられたものとして扱うので、その下のフレーム、つまりエラーが起きたメソッドもトレースから消えます。再スローは常に何も付けない throw; で行います。(NoInlining 属性があるのは、JITがこれほど小さなメソッドを呼び出し元に統合して、両方のトレースから消してしまうことがあるからにすぎません。)
代わりに文脈を加えたいなら、例外を新しい例外でラップし、元の例外を内部例外として渡します:throw new ConfigException("Could not start the app", e);。内部例外は独自のスタックトレースを保ち、ロガーは連鎖を出力します。独自の例外型の書き方は例外を投げるで扱います。
Exceptionオブジェクト
すべての例外は System.Exception から派生しています。よく使うメンバーは次のとおりです。
| メンバー | 保持しているもの |
|---|---|
Message | 人が読める説明 |
GetType().Name | FormatException のような例外の型 |
StackTrace | 例外が投げられた時点のメソッド呼び出しの連鎖 |
InnerException | この例外の原因となった例外、または null |
ToString() | 型、メッセージ、内部例外、スタックトレースをまとめたもの |
後で失敗を調査したいなら、e.Message ではなく e.ToString() を記録します。メッセージだけでは、問題がどこにあったかがわかることはまれです。e.ToString() をエンドユーザーに見せてはいけません。
よくある例外の型
| 例外 | 典型的な原因 |
|---|---|
NullReferenceException | null の参照に対してメンバーを呼び出した |
ArgumentNullException | 値が必要なメソッドに null が渡された |
ArgumentOutOfRangeException | 引数やリストのインデックスが許される範囲の外にある |
IndexOutOfRangeException | 配列のインデックスが範囲外 |
FormatException | 形式の誤ったテキストに対する int.Parse、DateTime.Parse など |
InvalidCastException | オブジェクトが実際にはその型でないのに明示的にキャストした |
InvalidOperationException | オブジェクトが呼び出しに適さない状態にある(空のシーケンス、変更されたコレクション) |
KeyNotFoundException | 存在しない辞書のキーをインデクサーで読んだ |
DivideByZeroException | 整数または decimal のゼロ除算 |
OverflowException | 解析した数値、checkedの変換、checkedの演算の結果が型に収まらない |
FileNotFoundException、IOException | ファイルシステムの問題 |
NullReferenceException、IndexOutOfRangeException、InvalidCastException は、ほぼ常にバグを意味します。それらはキャッチせず、コードを直します。
例外を握りつぶさない
空の catch は、予想していなかったものも含め、すべてのエラーを隠してしまいます。
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
プログラムは保存がうまくいったかのように動き続け、本当の原因は失われます。例外処理を誠実に保つための指針:
- 処理できるものだけを、それを処理できるレベルでキャッチします。
- 記録するためにキャッチした場合は、プログラムが本当に続行できるのでない限り、
throw;で再スローします。 Exceptionをキャッチするのは外側の端だけにします:Main、リクエストハンドラー、ワーカーのループ。- 想定内のケースには、例外より
TryParse、TryGetValue、nullチェックを優先します。例外を投げるのはチェックに比べて遅いですが、何も投げないtryブロックのコストはほぼゼロです。
よくある間違い
catch (Exception)を最初に置く。 後の句が到達できなくなります(CS0160)。- 再スローに
throw e;を使う。 元のスタックトレースが消えます。throw;を使います。 - 空のcatchブロック。 エラーが消えてしまいます。少なくとも記録して再スローします。
- 制御フローに例外を使う。
FormatExceptionをキャッチする代わりに、TryParseで入力を検証します。 - フレームワークの例外の
e.Messageをユーザーに見せる。 文言は.NETのバージョンによって異なり、開発者向けに書かれています。
よくある質問
C#のtry catchはどう動きますか?
失敗するかもしれないコードを try ブロックに入れます。そこで文が例外を投げると、ブロックの残りは飛ばされ、ランタイムは例外に型が一致する catch 句を、まず現在のメソッドで、次に各呼び出し元で探します。最初に一致した catch が実行され、実行は try 文全体の後から続きます。
C#のfinallyは必ず実行されますか?
ほぼ必ず実行されます。try ブロックが正常に終わった後、catch が例外を処理した後、ブロック内の return や break の後、そして例外が呼び出し履歴の上の catch へ向かう途中で通過するときです。実行されないのは、先にプロセスが終わる場合です。プロセスが強制終了された場合、Environment.FailFast、StackOverflowException、そして.NET Core以降では何もキャッチしない例外で、これは finally ブロックが実行される前にプロセスを終了させます。
C#で複数の例外をキャッチするには?
複数の catch 句を、最も限定的な型から順に書きます:catch (FileNotFoundException) を catch (IOException) の前に、それを catch (Exception) の前に置きます。前の句がすでにその型をキャッチしているために到達できない句は、コンパイラに拒否されます。関係のない2つの型を同じように処理するには、フィルターを使います:catch (Exception e) when (e is FormatException || e is OverflowException)。
C#のthrowとthrow exの違いは何ですか?
catch の中の throw; は、現在の例外を元のスタックトレースのまま再スローします。throw ex; は同じオブジェクトを投げますが、スタックトレースを現在の行にリセットするので、エラーが実際に起きたメソッドがトレースから消えます。throw; を使うか、ラップします:throw new MyException("context", ex);。
C#の例外フィルターとは何ですか?
catch の後の when 句です(C# 6以降):catch (HttpRequestException e) when (e.Message.Contains("404"))。catchブロックが実行されるのは条件が true のときだけです。そうでなければ、例外はその句がなかったかのように別のハンドラーを探し続け、スタックは巻き戻されません。
C#でExceptionをキャッチすべきですか?
プログラムの端でだけキャッチします。Main の最上部、リクエストハンドラー、バックグラウンドのループなど、エラーを記録して処理を続けるか、きれいに終了することが役目の場所です。コードの深い部分では、実際に処理できる特定の型をキャッチし、それ以外はすべて伝播させます。