例外と例外処理プロセスを調べる
- 11 分
C# アプリケーションのランタイム エラーは、例外と呼ばれるメカニズムを使用して管理されます。 例外は、システム レベルとアプリケーション レベルのエラー状態の両方を処理する、構造化された統一されたタイプ セーフな方法を提供します。 例外は、.NET ランタイムまたはアプリケーション内のコードによって生成されます。
例外処理を必要とする一般的なシナリオ
例外処理を必要とするプログラミング シナリオがいくつかあります。 これらのシナリオの多くは、何らかの形式のデータ取得を伴います。 一部のシナリオには、このトレーニングの範囲外のコーディング手法が含まれますが、それでも注目に値します。
例外処理を必要とする一般的なシナリオは次のとおりです。
ユーザー入力: コードがユーザー入力を処理するときに例外が発生する可能性があります。 たとえば、入力値の形式が正しくない場合や範囲外の場合、例外が発生します。
データ処理と計算: コードがデータの計算または変換を実行すると、例外が発生する可能性があります。 たとえば、コードが 0 で除算したり、サポートされていない型にキャストしたり、範囲外の値を割り当てたりすると、例外が発生します。
ファイルの入力/出力操作: コードがファイルから読み取ったり、ファイルに書き込んだりするときに例外が発生する可能性があります。 たとえば、例外は、ファイルが存在しない場合、プログラムにファイルにアクセスするためのアクセス許可がない場合、またはファイルが別のプロセスで使用されている場合に発生します。
データベース操作: コードがデータベースと対話するときに例外が発生する可能性があります。 たとえば、データベース接続が失われたり、SQL ステートメントで構文エラーが発生したり、制約違反が発生したりすると、例外が発生します。
ネットワーク通信: コードがネットワーク経由で通信するときに例外が発生する可能性があります。 たとえば、ネットワーク接続が失われたり、タイムアウトが発生したり、リモート サーバーからエラーが返されたりすると、例外が発生します。
その他の外部リソース: コードが他の外部リソースと通信するときに例外が発生する可能性があります。 Web サービス、REST API、またはサード パーティ製ライブラリは、さまざまな理由で例外をスローする可能性があります。 たとえば、ネットワーク接続の問題、形式が正しくないデータなどが原因で例外が発生します。
例外処理キーワード、コード ブロック、およびパターン
C# での例外処理は、 try、 catch、および finally キーワードを使用して実装されます。 これらの各キーワードには関連付けられたコード ブロックがあり、例外処理のアプローチで特定の目標を満たすために使用できます。 例えば次が挙げられます。
try
{
// try code block - code that may generate an exception
}
catch
{
// catch code block - code to handle an exception
}
finally
{
// finally code block - code to clean up resources
}
注
C# 言語では、 throw キーワードを使用してコードで例外オブジェクトを生成することもできます。
throw キーワードを使用して例外を生成する例外処理シナリオについては、Microsoft Learn の別のモジュールで説明します。
try コード ブロックには、例外が発生する可能性がある保護されたコードが含まれます。
try ブロック内のコードによって例外が発生した場合、例外は対応するcatch ブロックによって処理されます。
catch コード ブロックには、例外がキャッチされたときに実行されるコードが含まれます。
catch ブロックは、例外を処理したり、ログに記録したり、無視したりすることができます。
catch ブロックは、例外の種類が発生したとき、または特定の種類の例外が発生した場合にのみ実行するように構成できます。
finally コード ブロックには、例外が発生したかどうかを実行するコードが含まれています。
finally ブロックは、多くの場合、try ブロックに割り当てられているリソースをクリーンアップするために使用されます。 たとえば、変数に正しい値または必要な値が割り当てられていることを確認します。
C# アプリケーションでの例外処理は、通常、次の 1 つ以上のパターンを使用して実装されます。
-
try-catchパターンは、tryブロックとそれに続く 1 つ以上のcatch句で構成されます。 各catchブロックは、異なる例外のハンドラーを指定するために使用されます。 -
try-finallyパターンは、tryブロックとそれに続くfinallyブロックで構成されます。 通常、finallyブロックのステートメントは、コントロールがtryステートメントを離れると実行されます。 -
try-catch-finallyパターンは、3 種類の例外処理ブロックをすべて実装します。try-catch-finallyパターンの一般的なシナリオは、リソースが取得され、tryブロックで使用され、例外的な状況がcatchブロックで管理され、リソースが解放されるか、finallyブロックで管理される場合です。
例外はコードでどのように表されますか?
例外はコード内でオブジェクトとして表されます。つまり、例外はクラスのインスタンスであることを意味します。 .NET クラス ライブラリには、他の .NET クラスと同様にコードでアクセスされる例外クラスが用意されています。 コード内のオブジェクトとして使用される .NET クラスのもう 1 つの例は、 Random クラス (乱数の作成に使用) です。
より正確には、例外は型であり、最終的に System.Exceptionから派生したクラスによって表されます。
Exceptionから派生した例外クラスには、例外の種類を識別する情報と、例外に関する詳細を提供するプロパティが含まれています。
Exception クラスの詳細な検査については、このモジュールの後半で説明します。
クラスのランタイム インスタンスは一般にオブジェクトと呼ばれるため、例外は例外オブジェクトと呼ばれることがよくあります。
注
これらは同じ意味で使用されることもありますが、クラスとオブジェクトは異なります。 クラスはオブジェクトの型を定義しますが、オブジェクト自体ではありません。 オブジェクトは、クラスに基づく具象エンティティです。
例外処理プロセス
例外が発生すると、.NET ランタイムは、例外を処理できる最も近い catch 句を検索します。 プロセスは、例外がスローされる原因となったメソッドで始まります。 まず、例外の原因となったコードが try コード ブロック内にあるかどうかを調べます。 コードがコード ブロック内try場合、try ステートメントに関連付けられているcatch句は順番に考慮されます。
catch句で例外を処理できない場合は、現在のメソッドを呼び出したメソッドが検索されます。 このメソッドは、(最初のメソッドへの) メソッド呼び出しが try コード ブロック内にあるかどうかを判断するために調べます。 呼び出しが try コード ブロック内にある場合は、関連付けられている catch 句が考慮されます。 この検索プロセスは、現在の例外を処理できる catch 句が見つかるまで続行されます。
例外を処理できる catch 句が見つかったら、ランタイムは、 catch ブロックの最初のステートメントに制御を転送する準備をします。 ただし、catch ブロックの実行が開始される前に、ランタイムは、検索中に見つかったtryステートメントに関連付けられているすべてのfinallyブロックを実行します。 複数の finally ブロックが見つかった場合は、例外がスローされる原因となったコードに最も近いものから順に実行されます。
例外を処理する catch 句が見つからない場合、ランタイムはアプリケーションを終了し、エラー メッセージをユーザーに表示します。
try-catch パターン内に入れ子になったtry-finally パターンを含む次のコード サンプルを考えてみましょう。
try
{
// Step 1: code execution begins
try
{
// Step 2: an exception occurs here
}
finally
{
// Step 4: the system executes the finally code block associated with the try statement where the exception occurred
}
}
catch // Step 3: the system finds a catch clause that can handle the exception
{
// Step 5: the system transfers control to the first line of the catch code block
}
この例では、次のプロセスが発生します。
- 外側の
tryステートメントのコード ブロックで実行が開始されます。 - 内部
tryステートメントのコード ブロックで例外がスローされます。 - ランタイムは、外側の
tryステートメントに関連付けられているcatch句を検索します。 - ランタイムは、
catchコード ブロックの最初の行に制御を転送する前に、内部tryステートメントに関連付けられているfinally句を実行します。 - その後、ランタイムは、
catchコード ブロックの最初の行に制御を転送し、例外を処理するコードを実行します。
この単純な例では、入れ子になった try-catch パターンと try-finally パターンは 1 つのメソッド内に存在しますが、他のメソッドを呼び出すメソッド間で複数の try-catch パターンと try-finally パターンを分散できます。
例外処理と呼び出し履歴
例外処理と例外処理プロセスについて読むと、多くの場合、"呼び出し履歴のアンワインド" という用語が表示されます。 この用語を理解するには、呼び出し履歴と、コードの実行中にメソッド呼び出しの "スタック" を追跡するために使用される方法を理解する必要があります。
呼び出し履歴はブロックの塔のように考えることができます。 タワーを建てるときは、1 ブロックで始めます。 タワーにブロックを追加するたびに、既存のブロックの上に配置します。 アプリケーションがデバッガーで実行を開始すると、アプリケーションへのエントリ ポイントが、呼び出し履歴 (タワーの最初のブロック) に追加される最初のレイヤーになります。 メソッドが別のメソッドを呼び出すたびに、新しいメソッドがスタックの先頭に追加されます。 コードがメソッドから終了すると、メソッドは呼び出し履歴から削除されます。
注
コンソール アプリケーションの場合、アプリケーションへのエントリ ポイントは最上位レベルのステートメントです。 Visual Studio Code の呼び出し履歴では、このエントリ ポイントは Main メソッドと呼ばれます。
呼び出し履歴のアンワインドは、C# プログラムでエラーが発生したときに .NET ランタイムが使用するプロセスです。 これは、今確認したのと同じプロセスです。
ブロックタワーの例えに戻ると、タワーからブロックを削除する必要がある場合は、上部から始めて、必要なブロックに到達するまで各ブロックを削除します。 このプロセスは、呼び出し履歴のアンワインドのしくみに似ています。スタック内の各呼び出しレイヤーは、タワー内のブロックのようなものです。 ランタイムが呼び出し履歴をアンワインドする必要がある場合は、先頭から開始し、必要なものが含まれるものに到達するまで各呼び出しレイヤーを削除します。 この場合、必要な呼び出し層は、発生した例外を処理できる catch 句を持つメソッドです。
まとめ
このユニットで覚えておく必要があるいくつかの重要な点を次に示します。
- 例外処理が必要になる可能性がある一般的なシナリオには、ユーザー入力、データ処理、ファイル I/O 操作、データベース操作、ネットワーク通信などがあります。
- C# での例外処理は、
try、catch、およびfinallyキーワードを使用して実装されます。 各キーワードには、特定の目的に対応するコード ブロックが関連付けられています。 - 例外は型として表され、.NET の
System.Exceptionクラスから派生します。 例外には、例外の種類を識別する情報と、追加の詳細を提供するプロパティが含まれます。 - 例外が発生すると、.NET ランタイムは、それを処理できる最も近い
catch句を検索します。 検索は例外がスローされたメソッドから始まり、必要に応じて呼び出し履歴を下に移動します。