C#における例外処理は、アプリケーションの堅牢性と信頼性を確保するために欠かせない要素です。

特にtry-catch文はエラー発生時の挙動を制御するための中心的な仕組みですが、その使い方を誤るとバグの温床となる可能性があります。

本記事では、基本的な構文から、最新のC#仕様に基づいた例外処理のベストプラクティスまでを詳しく解説していきます。

try-catch文の基本構造と役割

try-catch文は、プログラムの実行中に発生する予期しないエラーを捕捉し、アプリケーションの異常終了を防ぐために使用されます。

基本的な構文は、例外が発生する可能性のあるコードを try ブロックで囲み、発生した例外を catch ブロックで処理するという形式をとります。

C#
try
{
    // 例外が発生する可能性のある処理
    int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
    // 例外が発生したときの処理
    Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
実行結果
エラーが発生しました: Attempted to divide by zero.

例外処理を適切に行うことで、システムがクラッシュすることなく安全に処理を継続、あるいは終了させることが可能になります。

finallyブロックによるリソースの解放

例外が発生したかどうかにかかわらず、必ず実行したい処理がある場合には finally ブロックを使用します。

例えば、ファイルのクローズやデータベース接続の解除など、リソースの確実な解放が必要な場面で非常に重要です。

C#
StreamReader reader = null;
try
{
    reader = new StreamReader("example.txt");
    Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("ファイルが見つかりません。");
}
finally
{
    // 例外の有無にかかわらず実行される
    reader?.Close();
    Console.WriteLine("リソースを解放しました。");
}

現代のC#では using ステートメントを使用することが推奨されますが、内部的にはこの try-finally 構造が働いています。

例外処理のベストプラクティス

try-catch文は強力な機能ですが、無闇に使用するとコードの可読性を下げ、パフォーマンスを悪化させる原因になります。

ここからは、プロフェッショナルな開発者が守るべき例外処理の原則について解説します。

特定の例外のみをキャッチする

最も重要なルールの1つは、すべての例外の基底クラスである Exception クラスを安易にキャッチしないことです。

予期しない例外まで握りつぶしてしまうと、プログラムが不安定な状態で動作を続けてしまい、原因の特定が困難になります。

C#
// アンチパターン:何でもキャッチしてしまう
try { /* 処理 */ }
catch (Exception) { /* ログも出さずに無視 */ }

// 推奨:発生し得る具体的な例外をキャッチする
try { /* 処理 */ }
catch (FileNotFoundException ex) { /* ファイル欠落時の対処 */ }
catch (UnauthorizedAccessException ex) { /* 権限エラー時の対処 */ }

このように、具体的な例外クラスを指定することで、それぞれの状況に応じた適切なエラーハンドリングが可能になります。

例外を再スローする際の注意点

catchブロック内で例外を処理しきれず、上位のメソッドに通知したい場合は、throw キーワードを使用します。

このとき、throw ex; と記述するとスタックトレースがリセットされてしまうため、注意が必要です。

元のスタックトレースを維持したまま再スローするには、引数なしの throw; を使用しなければなりません。

C#
try
{
    // 何らかの処理
}
catch (Exception ex)
{
    // ログ出力などの共通処理
    Log(ex.Message);

    // スタックトレースを維持して再スロー
    throw; 
}

以下の表は、例外の再スロー方法による違いをまとめたものです。

記述方法動作推奨度
throw;元の例外情報を保持して再スローする高(基本はこれ)
throw ex;例外を投げ直すが、発生源の場所が上書きされる低(デバッグが困難になる)
throw new MyException("error", ex);新しい例外でラップし、元の例外を InnerException に保持する中(カスタム例外が必要な場合)

例外フィルター(when句)の活用

C# 6.0以降では、when キーワードを使用した例外フィルターが導入されています。

これを利用すると、例外の型だけでなく、エラーメッセージの内容やステータスコードなどの条件に基づいてキャッチするかどうかを判定できます。

C#
try
{
    PerformNetworkOperation();
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    Console.WriteLine("リソースが見つかりませんでした。");
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
    Console.WriteLine("認証に失敗しました。");
}

例外フィルターを使用するメリットは、catchブロックの中に入ることなく条件判定が行われるため、デバッグ時のスタック状態がより正確に維持される点にあります。

また、複雑な条件分岐をcatchブロック内に記述する必要がなくなり、コードがすっきりと整理されます。

独自の例外クラスを作成する

標準の例外クラスでは表現しきれないドメイン固有のエラーを定義したい場合は、独自の例外クラスを作成します。

独自の例外クラスは Exception クラス(または InvalidOperationException などの適切な既存クラス)を継承して作成します。

C#
public class UserNotRegisteredException : Exception
{
    public UserNotRegisteredException() { }

    public UserNotRegisteredException(string message) : base(message) { }

    public UserNotRegisteredException(string message, Exception inner) : base(message, inner) { }
}

独自の例外を定義することで、呼び出し側で特定のエラーをより明確に区別してキャッチできるようになります。

ただし、カスタム例外を過剰に作成するとライブラリの複雑性が増すため、本当に必要な場合に限定して定義するのが賢明です。

例外処理のパフォーマンスへの影響

例外処理は非常に強力な仕組みですが、例外の発生(スロー)は計算コストが高い処理であることを忘れてはいけません。

例外は「予期しない異常事態」のために設計されており、通常のプログラムの流れ(制御フロー)として利用すべきではありません。

例えば、数値変換の処理において例外を使用する場合と、より効率的なメソッドを使用する場合を比較してみましょう。

C#
// 非推奨:例外を制御フローに利用する(遅い)
try
{
    int value = int.Parse(input);
}
catch (FormatException)
{
    // パース失敗時の処理
}

// 推奨:TryParseパターンを利用する(高速)
if (int.TryParse(input, out int result))
{
    // 成功時の処理
}
else
{
    // 失敗時の処理
}

このように、事前にチェックが可能なエラーについては、try-catchを使わずに条件分岐(if文)で対応するのが鉄則です。

非同期処理における例外処理

現代のC#開発では async/await を用いた非同期処理が一般的ですが、ここでも例外処理には注意が必要です。

async Task メソッド内で発生した例外は、そのメソッドを await した時点で再スローされます。

C#
public async Task ProcessDataAsync()
{
    try
    {
        await FetchRemoteDataAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"非同期処理中にエラー: {ex.Message}");
    }
}

注意すべき点は、async void メソッドです。

async void メソッドで発生した例外は、呼び出し元でキャッチすることができず、アプリケーション全体のクラッシュを引き起こす可能性があります。

イベントハンドラーなどのやむを得ない場合を除き、非同期メソッドは常に Task または ValueTask を返すように設計しましょう。

まとめ

C#の try-catch 文は、正しく使えばアプリケーションの堅牢性を高める強力な武器となります。

最後に、今回解説した重要なポイントを振り返ります。

  • 安易に Exception クラスをキャッチせず、具体的な例外を指定する。
  • リソースの解放には finallyusing ステートメントを確実に使用する。
  • 例外を再スローする場合は、スタックトレースを壊さない throw; を使用する。
  • 例外フィルター(when)を活用し、キャッチする条件を明確にする。
  • 例外を制御フローとして利用せず、パフォーマンスに配慮した TryParse パターンなどを検討する。
  • 非同期処理では async void を避け、適切に await して例外を捕捉する。

これらの原則を守ることで、デバッグが容易でメンテナンス性の高い、高品質なC#プログラムを構築することができるはずです。

日々のコーディングにおいて、常に「この例外は本当にここでキャッチすべきか」を自問自答する習慣をつけましょう。