C#: 例外処理
1. try/catch/finally の基本的な仕組み
C# では、try、catch、finally の 3 つのキーワードを使用して、例外処理メカニズムを構築します。 例外をスローする可能性のあるコードは try ブロック内に記述され、catch ブロックが例外をキャッチして処理し、finally ブロックは例外の発生の有無にかかわらず常に実行され、一般的にリソースのクリーンアップに使用されます。
try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
Console.WriteLine("finally block always executes");
}
Caught exception: Attempted to divide by zero.
finally block always executes
(1) 複数のキャッチブロック
さまざまな種類の例外を順番にキャッチすることができます。より具体的な例外の種類は、より一般的な例外の種類よりも前に配置する必要があります。
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}");
}
Index out of bounds: Index was outside the bounds of the array.
2. 一般的な例外の種類
| 例外の種類 | 発生シナリオ |
|---|---|
FormatException |
文字列の形式が不正です(例:int.Parse("abc")) |
NullReferenceException |
null オブジェクトのメンバへのアクセス |
IndexOutOfRangeException |
配列のインデックスが範囲外です |
DivideByZeroException |
整数をゼロで割る |
OverflowException |
算術オーバーフロー(チェック対象のコンテキスト内) |
FileNotFoundException |
ファイルが存在しません |
ArgumentException |
メソッドの引数が無効です |
▶ サンプル
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");
}
Format exception: string cannot be converted to integer
Null reference exception: object is null
3. 例外クラスの階層構造
すべての例外クラスの継承関係は次のとおりです。
Object
└─ Exception
└─ SystemException
├─ FormatException
├─ NullReferenceException
├─ IndexOutOfRangeException
├─ DivideByZeroException
├─ OverflowException
├─ FileNotFoundException
└─ ArgumentException
└─ ArgumentNullException
4. 例外クラスのプロパティ
Exception クラスには、詳細な例外情報を取得するために、以下のよく使われるプロパティが用意されています:
| プロパティ | 説明 |
|---|---|
Message |
例外を説明する人間が読みやすいテキスト |
StackTrace |
例外が発生した時点のコールスタック情報 |
InnerException |
現在の例外を引き起こした内部例外 |
Source |
例外をスローしたアプリケーションまたはオブジェクトの名前 |
TargetSite |
例外をスローしたメソッド |
▶ サンプル
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}");
}
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()
5. throw を使った例外の投げ方
throw キーワードを使用すると、例外を能動的にスローできます。再スローを行う場合、throw; は元のスタックトレースを保持しますが、throw ex; はスタックをリセットします。後者の使用は避けてください。
▶ サンプル
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);
}
Age cannot be negative (Parameter 'age')
(1) throw と throw ex の違い
void InnerMethod()
{
throw new InvalidOperationException("Internal error");
}
void OuterMethod()
{
try
{
InnerMethod();
}
catch (Exception ex)
{
throw;
}
}
try
{
OuterMethod();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
at InnerMethod() in Program.cs:line 2
at OuterMethod() in Program.cs:line 10
throw; を使用してください。これにより、呼び出しスタックが完全に保持されるため、根本原因の特定が容易になります。一方、throw ex; を使用すると、スタックが現在のメソッドまで切り詰められ、元の例外の発生位置情報が失われてしまいます。
6. 例外フィルター
C# 6 では、when 句による例外フィルターが導入され、catch に条件を追加できるようになりました。この例外は、その条件の評価結果が true である場合にのみ捕捉されます。
▶ サンプル
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}");
}
Caught timeout exception, preparing to retry
7. カスタム例外クラス
Exception を継承して、カスタム例外クラスを作成します。通常、3つのコンストラクタを実装する必要があります。すなわち、引数なしのコンストラクタ、メッセージ引数を持つコンストラクタ、およびメッセージと内部例外の両方の引数を持つコンストラクタです。
▶ サンプル
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}");
}
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200
8. 例外処理のベストプラクティス
(1) 単なる「例外」ではなく、特定の例外を捕捉する
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) 例外をスワローしない
try
{
File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}
上記のコードはすべてのエラーを隠蔽しており、良い慣行とは言えません。少なくとも、エラーをログに記録するか、再スローすべきです。
(3) finally を使ってリソースを解放する
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 を呼び出し、例外が発生した場合でもリソースの解放を確実に実行します。
using (var fs = new FileStream("data.txt", FileMode.Open))
{
int b = fs.ReadByte();
}
using ステートメントは、try-finally に Dispose() の呼び出しを加えたものと同等であり、リソース管理において推奨されるアプローチです。
(5) 例外を InnerException でラップする
try
{
SaveToFile();
}
catch (IOException ioEx)
{
throw new ApplicationException("Failed to save data", ioEx);
}
InnerException を使用することで、コンテキストを損なうことなく、元の例外情報が保持されます。
❓ よくある質問
throw; と throw ex; の違いは何ですか?throw; は元のスタックトレースを保持し、throw ex; はスタックを現在のメソッドにリセットします。 デバッグ時には throw; を使用してください。finally ブロックはどのような場合に実行されないのですか?Environment.FailFast)や、StackOverflowException が発生した場合、finally は実行されないことがあります。catch なしで try-finally を使用することはできますか?Exception を継承する必要がありますか?catch によってキャッチされるためには、Exception を(直接または間接的に)継承する必要があります。when の利点は何ですか?when 句は、条件に一致しない場合、例外をキャッチせず、例外の伝播を継続させます。 これは、catch 内で条件をチェックするよりも効率的であり、意味的にも明確です。📖 まとめ
tryブロックはエラーが発生する可能性のあるコードを囲み、catchは例外を捕捉し、finallyは常に実行されます- 一般的な例外タイプには、
FormatException、NullReferenceException、IndexOutOfRangeException、DivideByZeroExceptionなどがあります。 Exceptionクラスは、Message、StackTrace、InnerExceptionなどのプロパティを提供しますthrow;はスタックを保持し、throw ex;はスタックをリセットします。前者を使用してください。- カスタム例外クラスは
Exceptionを継承し、標準のコンストラクタを実装する - 例外フィルター
catch ... when (...)は、条件付きキャプチャ機能を提供します - 例外をスワローせず、特定の型をキャッチし、リソース管理には
usingを使用し、例外はInnerExceptionでラップする
📝 練習問題
try-catchを使用してFormatExceptionを処理し、0を返すメソッドint SafeParse(string s)を作成してください。また、解析に失敗した場合は警告を出力するようにしてください。- 成績が0~100の範囲外の場合にスローされる、
InvalidGradeExceptionというプロパティを持つカスタム例外InvalidGradeExceptionを作成する throw;とthrow ex;の違いを示すコードを記述し、両方のStackTraceの値を出力して、その違いを確認する- 例外フィルター
whenを使用して、ファイルパスが"config"で始まる場合にのみFileNotFoundExceptionをキャッチし、それ以外の場合は例外を伝播させるように実装する usingステートメントを使用してStreamReaderが適切に解放されるようにし、発生しうるFileNotFoundExceptionおよびIOExceptionに対処するファイル読み取りメソッドを作成してください。