LoginSignup
2
2

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

Last updated at Posted at 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)
{
	// 例外処理
}

参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2