【C#】その例外、使ってもいいけど使っちゃダメです

Sonar Lint を使わなければ気づかなかったシリーズ

Sonar Lint で警告を食らう度に記事を書いてる気がしますが、有用な情報なのでスルーしない方が良いと思います。

今回問題になったのは以下のコードです。

throw new System.NullReferenceException();

…って、例外投げてるだけなんですけどね。

何が問題なのか?何故問題なのか?
NullReferenceException をスローすることの問題点。

以下は、Sonar Lint の警告 S112 説明ページからの引用を日本語に翻訳したものです。

さらに、いくつかの予約例外は手動で投げてはいけません。IndexOutOfRangeException、NullReferenceException、OutOfMemoryException、ExecutionEngineException などの例外は、対応するエラーが発生すると、ランタイムによって自動的にスローされます。これらの例外の多くは深刻なエラーを示しており、アプリケーションが回復できない可能性があります。従って、これらを基底クラスとして使用するだけでなく、スローしないことを推奨します。

powered by DeepL.com(無料版)

使えるけど、使っちゃダメです。

とのこと…。

どういう状況で例外をスローするべきか?

以下も、Sonar Lint の警告 S112 説明ページからの引用を日本語に翻訳したものです。

Exception、SystemException、ApplicationException などの一般的な例外をスローすると、これらの例外をキャッチしようとするコードに悪影響を及ぼします。

コンシューマの観点からは、一般的に、自分が処理するつもりの例外だけをキャッチするのがベスト・プラクティスです。それ以外の例外は、スタック・トレースを伝播させて適切に処理できるようにするのが理想的です。一般的な例外がスローされると、コンシューマは自分が処理するつもりのない例外をキャッチせざるを得なくなり、再スローしなければならなくなる。

その上、一般的な例外を扱う場合、複数の例外を区別する唯一の方法は、そのメッセージをチェックすることである。正当な例外は意図せずに黙殺され、エラーは隠されるかもしれない。

例えば、StackOverflowExceptionのような例外がキャッチされたのに再スローされないと、プログラムが優雅に終了できなくなる可能性がある。

したがって、例外を投げるときは、コンシューマが意図的に処理できるように、可能な限り具体的な例外を投げることを推奨する。

powered by DeepL.com(無料版)

要約すると

スローした例外をキャッチするつもりがないなら、そもそもスローするべきではない。

もし例外をスローするなら、例外のクラス名とメッセージを見るだけで、プログラムのどこで起きたのか?何故起きたのか?この例外を見た人はどのように対処すれば良いのか?が分かる情報を持たせたカスタム例外を使うべき。

例外を見るのはプログラマーだけとは限らないです。
プログラムの知識がないユーザーが見ることが想定されるなら、そのユーザーが困らないように、メッセージに連絡先を入れたり、アプリの再起動方法を細かく書く必要があるかも知れません。

キャッチするときの注意点

Task の終了待ちの仕方がよく分からなかったときに、以下のように終了待ちをしていました。

class Program {
	static void Main() {
		try {
			var t = new ExceptionTest();
			t.Run();
		} catch (Exception e) {
			Console.WriteLine("Exception thrown:\n" + e.ToString());
		}
	}
}
class ExceptionTest {
	public void Run() {
		var task = Task.Run(this.ProcessOtherThread);

		//task.Wait(); なら問題ない

		while (!task.IsCanceled && !task.IsFaulted && !task.IsCompleted) {
			//	このループの中にいるときに例外がスローされるとキャッチできない
			Task.Delay(1);
		}
	}
	void ProcessOtherThread() {
		Thread.Sleep(10);
		throw new NotImplementedException();
	}
}

タスクの終了待ちを while で行うと、例外が Main() まで伝搬しません。
ProcessOtherThread() で例外が投げられても、何故か例外が投げられていない扱いになり、普通に while ループを抜けてしまいます。
理由は不明です。
Task は Wait か WaitAsync で終了待ちしないとダメみたいです。

Thread を使えばループで終了待ちしても問題ないです。
以下は Task を Thread に変更しただけですが、ProcessOtherThread() で投げられた例外は Main() まで正しく伝搬されます。

class ExceptionTest {
	public void Run() {
		var t = new Thread(new ThreadStart(this.ProcessOtherThread));
		t.Start();

		Console.WriteLine("Thread started");

		while (t.IsAlive) {
			Thread.Sleep(1);
		}
	}
	void ProcessOtherThread() {
		Thread.Sleep(10);
		throw new NotImplementedException();
	}
}
関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です