[C#] 例外処理の基本とベストプラクティス
C#における例外処理は、プログラムの実行中に発生するエラーを適切に管理するための重要な機能です。
基本的には、try
ブロックで例外が発生する可能性のあるコードを囲み、catch
ブロックでその例外をキャッチして処理します。
finally
ブロックを使用すると、例外の有無にかかわらず必ず実行したいコードを記述できます。
ベストプラクティスとしては、特定の例外をキャッチする、例外メッセージをログに記録する、例外を再スローする場合は適切な情報を追加する、リソースの解放をfinally
で行うなどがあります。
これにより、コードの信頼性と保守性が向上します。
例外処理の基本
例外とは何か
例外とは、プログラムの実行中に発生する予期しないエラーや異常状態のことを指します。
これらは通常、プログラムの正常な流れを妨げるものであり、適切に処理しないとプログラムがクラッシュする原因となります。
C#では、例外はSystem.Exceptionクラス
を基底クラスとするオブジェクトとして表現されます。
例外の種類
C#における例外は、主に以下のような種類に分類されます。
例外の種類 | 説明 |
---|---|
System.Exception | すべての例外の基底クラス |
System.SystemException | システムレベルの例外の基底クラス |
System.ApplicationException | アプリケーションレベルの例外の基底クラス |
System.NullReferenceException | オブジェクト参照が設定されていない場合に発生 |
System.IndexOutOfRangeException | 配列のインデックスが範囲外の場合に発生 |
例外処理の基本構文
try-catchブロック
try-catch
ブロックは、例外をキャッチして処理するための基本的な構文です。
try
ブロック内で例外が発生した場合、catch
ブロックでその例外をキャッチして処理します。
using System;
class Program
{
static void Main()
{
try
{
// 例外を発生させるコード
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[3]); // 範囲外アクセス
}
catch (IndexOutOfRangeException ex)
{
// 例外処理
Console.WriteLine("インデックスが範囲外です: " + ex.Message);
}
}
}
インデックスが範囲外です: Index was outside the bounds of the array.
この例では、配列の範囲外アクセスによってIndexOutOfRangeException
が発生し、catch
ブロックで処理されています。
finallyブロック
finally
ブロックは、例外の発生有無にかかわらず、必ず実行されるコードを記述するために使用します。
リソースの解放などに利用されます。
using System;
class Program
{
static void Main()
{
try
{
int divVal = 0;
// 例外を発生させるコード
int result = 10 / divVal; // ゼロ除算
}
catch (DivideByZeroException ex)
{
// 例外処理
Console.WriteLine("ゼロで除算しました: " + ex.Message);
}
finally
{
// 必ず実行されるコード
Console.WriteLine("プログラム終了時に必ず実行されます。");
}
}
}
ゼロで除算しました: Attempted to divide by zero.
プログラム終了時に必ず実行されます。
この例では、finally
ブロック内のコードが例外の発生に関係なく実行されます。
throw文
throw
文は、例外を明示的に発生させるために使用します。
これにより、特定の条件下で例外を発生させることができます。
using System;
class Program
{
static void Main()
{
try
{
// 条件に基づいて例外を発生させる
ThrowIfNegative(-1);
}
catch (ArgumentException ex)
{
// 例外処理
Console.WriteLine("例外が発生しました: " + ex.Message);
}
}
static void ThrowIfNegative(int number)
{
if (number < 0)
{
// 例外を発生させる
throw new ArgumentException("負の数は許可されていません。");
}
}
}
例外が発生しました: 負の数は許可されていません。
この例では、ThrowIfNegativeメソッド
内で負の数が渡された場合にArgumentException
を発生させています。
例外処理のベストプラクティス
特定の例外をキャッチする
例外処理においては、特定の例外をキャッチすることが重要です。
一般的なExceptionクラス
をキャッチするのではなく、発生する可能性のある具体的な例外をキャッチすることで、より適切なエラーハンドリングが可能になります。
using System;
class Program
{
static void Main()
{
try
{
// 例外を発生させるコード
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[3]); // 範囲外アクセス
}
catch (IndexOutOfRangeException ex)
{
// 特定の例外をキャッチ
Console.WriteLine("インデックスが範囲外です: " + ex.Message);
}
catch (Exception ex)
{
// その他の例外をキャッチ
Console.WriteLine("予期しないエラーが発生しました: " + ex.Message);
}
}
}
この例では、IndexOutOfRangeException
を特定してキャッチし、それ以外の例外は一般的なException
でキャッチしています。
例外メッセージのログ記録
例外が発生した際には、その詳細をログに記録することが重要です。
これにより、後で問題を分析し、修正するための情報を得ることができます。
using System;
using System.IO;
class Program
{
static void Main()
{
try
{
int num = 0;
// 例外を発生させるコード
int result = 10 / num; // ゼロ除算
}
catch (DivideByZeroException ex)
{
// 例外メッセージをログに記録
LogException(ex);
}
}
static void LogException(Exception ex)
{
// ログファイルに例外メッセージを記録
File.WriteAllText("error.log", DateTime.Now + " - " + ex.Message);
Console.WriteLine("例外がログに記録されました。");
}
}
この例では、DivideByZeroException
が発生した際に、そのメッセージをログファイルに記録しています。
例外の再スロー
例外をキャッチした後、再度スローすることで、上位の呼び出し元に例外を伝播させることができます。
これにより、例外の詳細を保持したまま、より高いレベルでの処理が可能になります。
using System;
class Program
{
static void Main()
{
try
{
// 例外を発生させるメソッドを呼び出し
ProcessData();
}
catch (Exception ex)
{
// 上位で例外をキャッチ
Console.WriteLine("上位で例外をキャッチしました: " + ex.Message);
}
}
static void ProcessData()
{
try
{
// 例外を発生させるコード
int result = 10 / 0; // ゼロ除算
}
catch (DivideByZeroException ex)
{
// 例外を再スロー
Console.WriteLine("例外を再スローします。");
throw;
}
}
}
この例では、ProcessDataメソッド
内で例外をキャッチし、再スローすることで、Mainメソッド
で例外をキャッチしています。
リソースの解放
例外が発生した場合でも、使用したリソースを適切に解放することが重要です。
finally
ブロックを使用することで、例外の有無にかかわらずリソースを解放できます。
using System;
using System.IO;
class Program
{
static void Main()
{
StreamReader reader = null;
try
{
// ファイルを開く
reader = new StreamReader("example.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
// 例外処理
Console.WriteLine("ファイルが見つかりません: " + ex.Message);
}
finally
{
// リソースの解放
if (reader != null)
{
reader.Close();
Console.WriteLine("リソースが解放されました。");
}
}
}
}
この例では、StreamReader
を使用してファイルを読み込んでいますが、例外が発生してもfinally
ブロックでリソースを解放しています。
カスタム例外の作成
特定の状況に応じたカスタム例外を作成することで、より明確なエラーメッセージを提供し、エラーハンドリングを改善できます。
using System;
class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message)
{
}
}
class Program
{
static void Main()
{
try
{
// カスタム例外を発生させる
ValidateAge(-1);
}
catch (InvalidAgeException ex)
{
// カスタム例外をキャッチ
Console.WriteLine("カスタム例外が発生しました: " + ex.Message);
}
}
static void ValidateAge(int age)
{
if (age < 0)
{
// カスタム例外を発生させる
throw new InvalidAgeException("年齢は負の数にできません。");
}
}
}
この例では、InvalidAgeException
というカスタム例外を作成し、年齢が負の数の場合に発生させています。
例外処理の応用
非同期プログラミングでの例外処理
非同期プログラミングでは、async
とawait
を使用して非同期メソッドを実装します。
非同期メソッド内で例外が発生した場合、await
で待機している箇所で例外がスローされます。
これにより、非同期メソッドの呼び出し元で例外をキャッチすることができます。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
// 非同期メソッドの呼び出し
await PerformAsyncOperation();
}
catch (Exception ex)
{
// 非同期メソッド内の例外をキャッチ
Console.WriteLine("非同期メソッドで例外が発生しました: " + ex.Message);
}
}
static async Task PerformAsyncOperation()
{
await Task.Delay(1000); // 非同期処理のシミュレーション
throw new InvalidOperationException("無効な操作が行われました。");
}
}
この例では、PerformAsyncOperationメソッド
内で例外が発生し、Mainメソッド
でキャッチされています。
LINQでの例外処理
LINQクエリを使用する際にも例外が発生することがあります。
特に、クエリの実行時にデータソースが不正な状態である場合などです。
LINQクエリの実行時に例外をキャッチすることで、エラーハンドリングを行うことができます。
using System;
using System.Linq;
class Program
{
static void Main()
{
try
{
// LINQクエリの実行
var numbers = new int[] { 1, 2, 3, 0 };
var result = numbers.Select(n => 10 / n).ToList();
}
catch (DivideByZeroException ex)
{
// LINQクエリ内の例外をキャッチ
Console.WriteLine("LINQクエリでゼロ除算が発生しました: " + ex.Message);
}
}
}
この例では、LINQクエリ内でゼロ除算が発生し、例外がキャッチされています。
マルチスレッド環境での例外処理
マルチスレッド環境では、スレッドごとに例外をキャッチする必要があります。
スレッド内で例外が発生した場合、そのスレッド内で例外をキャッチし、適切に処理することが重要です。
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
thread.Join(); // スレッドの終了を待機
}
static void DoWork()
{
try
{
int num = 0;
// スレッド内で例外を発生させる
int result = 10 / num; // ゼロ除算
}
catch (DivideByZeroException ex)
{
// スレッド内の例外をキャッチ
Console.WriteLine("スレッド内でゼロ除算が発生しました: " + ex.Message);
}
}
}
この例では、DoWorkメソッド
内で例外が発生し、そのスレッド内でキャッチされています。
スレッド内で例外をキャッチすることで、プログラム全体のクラッシュを防ぐことができます。
まとめ
この記事では、C#における例外処理の基本からベストプラクティス、そして応用までを詳しく解説しました。
例外処理は、プログラムの安定性を保ち、予期しないエラーに対処するための重要な技術です。
これを機に、実際のプロジェクトで例外処理を適切に実装し、より堅牢なコードを書くことに挑戦してみてください。