Effective C# 6.0/7.0 メモ - 第 5 章 例外処理
この記事は「Effective C# 6.0/7.0」の読書メモとして、私的プラクティスをまとめています。特に重要だと感じた項目のみ簡潔にまとめています。より詳細な内容に興味のある方は、原著を読んでみることをお勧めします。
項目 45 契約違反を例外として報告すること
エラー通知に便利な例外ですが、何でも例外にすればいいというわけではありません。適切な catch 句が見つからなかった場合、スローされた例外によってアプリケーションが強制終了してしまいます。
想定内のエラーか、想定外のエラーかを区別しましょう。想定内のエラーであれば、アプリケーション固有の例外を作成したり、Try メソッドによる事前チェックを行うなどして対応しましょう。
Try メソッドによる事前チェック
失敗する可能性のある処理を TryGet や TryParse のような Try メソッドにすることで、失敗を例外ではなく戻り値で行うことができます。
// 失敗する可能性のあるタスク
var worker = new MyWorker();
// NG: work()で想定外の例外が発生してもcatchで握りつぶしてしまう
try
{
worker.DoWork();
// work()が成功したときの処理
}
catch (Exception e)
{
// work()が失敗したときの処理
}
// OK: 想定外の例外はスローされる
if (worker.TryDoWork())
{
// work()が成功したときの処理
}
else
{
// work()が失敗したときの処理
}
項目 46 using および try...finally を使用してリソースの後処理を行う
アンマネージリソースを扱う際の using を try...finally で書き換えると以下のようになります。この using と try...finally からは同じ IL が生成されます。
SqlConnection connection = null;
// using
using (connection = new SqlConnection("..."))
{
// ...
}
// try...finally
try
{
connection = new SqlConnection("...");
// ...
}
finally
{
connection.Dispose()
}
基本的には using を使うことになると思いますが、複数のアンマネージリソースを扱うときには try...finally の方が記述しやすいかもしれません。
ちなみに、アンマネージリソースを扱うクラスに Close() と Dispose() が両方定義されている場合には、Dispose() を呼ぶようにしましょう。これは 2 章で説明したファイナライザを抑制するためです。
項目 47 アプリケーション固有の例外クラスを作成する
アプリケーション固有の例外を定義することによって、それぞれのエラーにおいて適切な処理を行うことができます。
try
{
Foo();
}
catch (FirstException e1)
{
// FirstException
}
catch (SecondException e2)
{
// SecondException
}
catch (Exception e)
{
throw;
}
アプリケーション固有の例外を作成する
独自の例外を作成するときは以下のルールに従うことが推奨されます。
- クラス名は「Exception」で終わる
- Exception クラスの 4 つのコンストラクタを実装する
- Serializable 属性を付与する
[Serializable]
public class ApplicationException : Exception
{
// 既定のコンストラクタ
public ApplicationException() : base() { }
// メッセージを指定して例外を作成
public ApplicationException(string message) : base(message) { }
// メッセージと内部例外を指定して例外を作成
public ApplicationException(string message, Exception innerException) : base(message, innerException) { }
// 入力ストリームから例外を作成
// .NET Core プラットフォームではサポートされない場合がある
protected ApplicationException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
例外翻訳を行う
サードパーティ製のライブラリから例外が発生する可能性がある場合、アプリケーション固有の情報を付加しつつ、元の例外を InnerException として含むような独自の例外をスローしましょう。このテクニックは例外翻訳と呼ばれています。
public double DoSomething()
{
try
{
// 以下ではサードパーティ製のライブラリから例外がスローされる可能性がある
return ThirdPartyLibrary.ImportantRoutine();
}
catch (ThirdPartyException e)
{
string msg = $"ライブラリの使用中に{ToString()}で問題が発生しました";
throw new DoSomethingException(msg, e);
}
}
項目 48 例外を強く保証すること
例外の発生後に「プログラムがどのような状態になっているか」を決める基準となるのが、例外保証です。
3 種類の例外保証
Basic 保証が最も弱く、no-fail 保証が最も強い例外保証となります。保証が強いほどエラーからの復旧は容易になりますが、一方で処理の複雑さが増すというトレードオフとなります。処理の内容によって適切な例外保証レベルを基準としましょう。
- Basic 保証: リソースリークを発生させず、すべてのオブジェクトが正しい状態である
- Strong 保証: プログラムの状態が変化しない
- no-throw 保証: 例外をスローしない
no-throw 保証
no-throw 保証を行うべき箇所は多くありません。以下がその箇所となります。
- Dispose()
- ファイナライザ
- 例外フィルタ(
catch
句内のwhen
) - デリゲート(※マルチキャストデリゲートでは、例外がスローされると後続の処理が中断してしまう)
項目 49 catch からの再スローよりも例外フィルタを使用すること
キャッチする例外に条件を加えたいときは例外フィルタ(catch
句内の when
)を使いましょう。
// NG: catchからの再スローを行うことによって、スタックトレースが失われる
try
{
// ...
}
catch (Exception e)
{
if (e.Message.Contains("foo"))
{
// ...
}
else
{
throw;
}
}
// OK
try
{
// ...
}
catch (Exception e) when (e.Message.Contains("foo"))
{
// ...
}
項目 50 例外フィルタの副作用を活用する
例外フィルタの性質を利用することで、以下のような処理が可能になります。
- 例外発生時に常に実行したい処理をはさむ
- デバッガがアタッチされているかどうかによって処理を変える
try
{
// ...
}
// 発生した例外をログ出力する(戻り値は常にfalse)
catch (Exception e) when (LogException(e)) { }
// デバッガがアタッチされているときはスロー
catch (TimeoutException e) when (failures++ < 10 && !System.Diagnostics.Debugger.IsAttached)
{
// 例外処理
}
Discussion