🐙

Effective C# 6.0/7.0 メモ - 第 5 章 例外処理

2023/08/29に公開

この記事は「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 保証が最も強い例外保証となります。保証が強いほどエラーからの復旧は容易になりますが、一方で処理の複雑さが増すというトレードオフとなります。処理の内容によって適切な例外保証レベルを基準としましょう。

  1. Basic 保証: リソースリークを発生させず、すべてのオブジェクトが正しい状態である
  2. Strong 保証: プログラムの状態が変化しない
  3. no-throw 保証: 例外をスローしない

no-throw 保証

no-throw 保証を行うべき箇所は多くありません。以下がその箇所となります。

  1. Dispose()
  2. ファイナライザ
  3. 例外フィルタ(catch 句内の when
  4. デリゲート(※マルチキャストデリゲートでは、例外がスローされると後続の処理が中断してしまう)

項目 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