C#: 例外処理

1. try/catch/finally の基本的な仕組み

C# では、trycatchfinally の 3 つのキーワードを使用して、例外処理メカニズムを構築します。 例外をスローする可能性のあるコードは try ブロック内に記述され、catch ブロックが例外をキャッチして処理し、finally ブロックは例外の発生の有無にかかわらず常に実行され、一般的にリソースのクリーンアップに使用されます。

CSHARP
try
{
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
    Console.WriteLine("finally block always executes");
}
TEXT 📖 参照専用
Caught exception: Attempted to divide by zero.
finally block always executes

(1) 複数のキャッチブロック

さまざまな種類の例外を順番にキャッチすることができます。より具体的な例外の種類は、より一般的な例外の種類よりも前に配置する必要があります。

CSHARP
try
{
    int[] arr = { 1, 2, 3 };
    Console.WriteLine(arr[10]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Index out of bounds: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"General exception: {ex.Message}");
}
TEXT 📖 参照専用
Index out of bounds: Index was outside the bounds of the array.

2. 一般的な例外の種類

例外の種類 発生シナリオ
FormatException 文字列の形式が不正です(例:int.Parse("abc")
NullReferenceException null オブジェクトのメンバへのアクセス
IndexOutOfRangeException 配列のインデックスが範囲外です
DivideByZeroException 整数をゼロで割る
OverflowException 算術オーバーフロー(チェック対象のコンテキスト内)
FileNotFoundException ファイルが存在しません
ArgumentException メソッドの引数が無効です

▶ サンプル

CSHARP
try
{
    int num = int.Parse("hello");
}
catch (FormatException)
{
    Console.WriteLine("Format exception: string cannot be converted to integer");
}

string s = null;
try
{
    Console.WriteLine(s.Length);
}
catch (NullReferenceException)
{
    Console.WriteLine("Null reference exception: object is null");
}
▶ 試してみよう
TEXT 📖 参照専用
Format exception: string cannot be converted to integer
Null reference exception: object is null

3. 例外クラスの階層構造

すべての例外クラスの継承関係は次のとおりです。

TEXT 📖 参照専用
Object
  └─ Exception
       └─ SystemException
            ├─ FormatException
            ├─ NullReferenceException
            ├─ IndexOutOfRangeException
            ├─ DivideByZeroException
            ├─ OverflowException
            ├─ FileNotFoundException
            └─ ArgumentException
                 └─ ArgumentNullException

4. 例外クラスのプロパティ

Exception クラスには、詳細な例外情報を取得するために、以下のよく使われるプロパティが用意されています:

プロパティ 説明
Message 例外を説明する人間が読みやすいテキスト
StackTrace 例外が発生した時点のコールスタック情報
InnerException 現在の例外を引き起こした内部例外
Source 例外をスローしたアプリケーションまたはオブジェクトの名前
TargetSite 例外をスローしたメソッド

▶ サンプル

CSHARP
try
{
    int zero = 0;
    int result = 100 / zero;
}
catch (Exception ex)
{
    Console.WriteLine($"Message: {ex.Message}");
    Console.WriteLine($"Source: {ex.Source}");
    Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
▶ 試してみよう
TEXT 📖 参照専用
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()

5. throw を使った例外の投げ方

throw キーワードを使用すると、例外を能動的にスローできます。再スローを行う場合、throw; は元のスタックトレースを保持しますが、throw ex; はスタックをリセットします。後者の使用は避けてください。

▶ サンプル

CSHARP
void CheckAge(int age)
{
    if (age < 0)
    {
        throw new ArgumentException("Age cannot be negative", nameof(age));
    }
    Console.WriteLine($"Age: {age}");
}

try
{
    CheckAge(-5);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}
▶ 試してみよう
TEXT 📖 参照専用
Age cannot be negative (Parameter 'age')

(1) throwthrow ex の違い

CSHARP
void InnerMethod()
{
    throw new InvalidOperationException("Internal error");
}

void OuterMethod()
{
    try
    {
        InnerMethod();
    }
    catch (Exception ex)
    {
        throw;
    }
}

try
{
    OuterMethod();
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
}
TEXT 📖 参照専用
   at InnerMethod() in Program.cs:line 2
   at OuterMethod() in Program.cs:line 10
💡 ヒント: 例外を再スローする際は、常に throw; を使用してください。これにより、呼び出しスタックが完全に保持されるため、根本原因の特定が容易になります。一方、throw ex; を使用すると、スタックが現在のメソッドまで切り詰められ、元の例外の発生位置情報が失われてしまいます。


6. 例外フィルター

C# 6 では、when 句による例外フィルターが導入され、catch に条件を追加できるようになりました。この例外は、その条件の評価結果が true である場合にのみ捕捉されます。

▶ サンプル

CSHARP
try
{
    throw new HttpRequestException("Network timeout, please retry later");
}
catch (HttpRequestException ex) when (ex.Message.Contains("timeout"))
{
    Console.WriteLine("Caught timeout exception, preparing to retry");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Other network exception: {ex.Message}");
}
▶ 試してみよう
TEXT 📖 参照専用
Caught timeout exception, preparing to retry

7. カスタム例外クラス

Exception を継承して、カスタム例外クラスを作成します。通常、3つのコンストラクタを実装する必要があります。すなわち、引数なしのコンストラクタ、メッセージ引数を持つコンストラクタ、およびメッセージと内部例外の両方の引数を持つコンストラクタです。

▶ サンプル

CSHARP 📖 参照専用
class InsufficientBalanceException : Exception
{
    public decimal Balance { get; }
    public decimal Amount { get; }

    public InsufficientBalanceException()
        : base("Insufficient balance") { }

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

    public InsufficientBalanceException(string message, Exception innerException)
        : base(message, innerException) { }

    public InsufficientBalanceException(decimal balance, decimal amount)
        : base($"Insufficient balance: current balance {balance}, required {amount}")
    {
        Balance = balance;
        Amount = amount;
    }
}

class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal balance)
    {
        Balance = balance;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
        {
            throw new InsufficientBalanceException(Balance, amount);
        }
        Balance -= amount;
        Console.WriteLine($"Withdrawal successful, balance: {Balance}");
    }
}

var account = new BankAccount(100);
try
{
    account.Withdraw(200);
}
catch (InsufficientBalanceException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Balance: {ex.Balance}, attempted withdrawal: {ex.Amount}");
}
論理コード 44 行(40 行制限超過、参照専用)
TEXT 📖 参照専用
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200

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

(1) 単なる「例外」ではなく、特定の例外を捕捉する

CSHARP
try
{
    int value = int.Parse(input);
    Console.WriteLine(100 / value);
}
catch (FormatException)
{
    Console.WriteLine("Input is not a valid integer");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Cannot input zero");
}
⚠️ 注意: 例外を隠蔽するために空の catch ブロックを使用しないでください。これにより、問題の特定が困難になります。

(2) 例外をスワローしない

CSHARP
try
{
    File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}
▶ 試してみよう

上記のコードはすべてのエラーを隠蔽しており、良い慣行とは言えません。少なくとも、エラーをログに記録するか、再スローすべきです。

(3) finally を使ってリソースを解放する

CSHARP
FileStream fs = null;
try
{
    fs = new FileStream("data.txt", FileMode.Open);
    int b = fs.ReadByte();
}
finally
{
    fs?.Dispose();
}

(4) try-finally の代わりに using ステートメントを使用する

IDisposable インターフェースを実装するリソースは、using ステートメントを優先して使用すべきです。このステートメントは、スコープの終了時に自動的に Dispose を呼び出し、例外が発生した場合でもリソースの解放を確実に実行します。

CSHARP
using (var fs = new FileStream("data.txt", FileMode.Open))
{
    int b = fs.ReadByte();
}
📌 要点: using ステートメントは、try-finallyDispose() の呼び出しを加えたものと同等であり、リソース管理において推奨されるアプローチです。

(5) 例外を InnerException でラップする

CSHARP
try
{
    SaveToFile();
}
catch (IOException ioEx)
{
    throw new ApplicationException("Failed to save data", ioEx);
}
▶ 試してみよう

InnerException を使用することで、コンテキストを損なうことなく、元の例外情報が保持されます。

❓ よくある質問

Q throw;throw ex; の違いは何ですか?
A throw; は元のスタックトレースを保持し、throw ex; はスタックを現在のメソッドにリセットします。 デバッグ時には throw; を使用してください。
Q finally ブロックはどのような場合に実行されないのですか?
A プログラムが強制終了された場合(例:Environment.FailFast)や、StackOverflowException が発生した場合、finally は実行されないことがあります。
Q catch なしで try-finally を使用することはできますか?
A はい。この方法では例外は処理されませんが、リソースのクリーンアップは確実に行われます。例外は引き続き上位へ伝播します。
Q カスタム例外クラスは Exception を継承する必要がありますか?
A はい、すべての例外クラスは、catch によってキャッチされるためには、Exception を(直接または間接的に)継承する必要があります。
Q 例外フィルター when の利点は何ですか?
A when 句は、条件に一致しない場合、例外をキャッチせず、例外の伝播を継続させます。 これは、catch 内で条件をチェックするよりも効率的であり、意味的にも明確です。

📖 まとめ

📝 練習問題

  1. try-catch を使用して FormatException を処理し、0 を返すメソッド int SafeParse(string s) を作成してください。また、解析に失敗した場合は警告を出力するようにしてください。
  2. 成績が0~100の範囲外の場合にスローされる、InvalidGradeExceptionというプロパティを持つカスタム例外InvalidGradeExceptionを作成する
  3. throw;throw ex; の違いを示すコードを記述し、両方の StackTrace の値を出力して、その違いを確認する
  4. 例外フィルター when を使用して、ファイルパスが "config" で始まる場合にのみ FileNotFoundException をキャッチし、それ以外の場合は例外を伝播させるように実装する
  5. using ステートメントを使用して StreamReader が適切に解放されるようにし、発生しうる FileNotFoundException および IOException に対処するファイル読み取りメソッドを作成してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%