演習 - 特定の例外の種類をキャッチする
このモジュールの前半で、C# アプリケーションがキャッチする例外オブジェクトは例外クラスのインスタンスであることを学習しました。 一般に、コードは次のいずれかを catch
します。
System.Exception
基底クラスのインスタンスである例外オブジェクト。- 基底クラスを継承する例外の種類のインスタンスである例外オブジェクト。 たとえば、
InvalidCastException
クラスのインスタンスです。
例外のプロパティを調べる
System.Exception
は、すべての派生例外の種類が継承する基底クラスです。 各例外の種類は、特定のクラス階層を介して基底クラスから継承されます。 たとえば、InvalidCastException
のクラス階層は次のようになります。
Object
Exception
SystemException
InvalidCastException
Exception
から継承する例外クラスのほとんどは、機能を追加するのではなく、単に Exception
を継承します。 そのため、Exception
クラスのプロパティを調べることで、ほとんどの例外と、コードで例外を使う方法を理解できます。
Exception
クラスのプロパティは次のとおりです。
- Data:
Data
プロパティは、キーと値のペアで任意のデータを保持します。 - HelpLink:
HelpLink
プロパティを使って、例外の原因に関する広範な情報を提供するヘルプ ファイルの URL (または URN) を保持できます。 - HResult:
HResult
プロパティを使って、特定の例外に割り当てられたコード化された数値にアクセスできます。 - InnerException:
InnerException
プロパティを使って、例外処理中に一連の例外を作成して保持できます。 - Message:
Message
プロパティには、例外の原因に関する詳細を指定できます。 - ソース:
Source
プロパティを使って、エラーの原因になったアプリケーションまたはオブジェクトの名前にアクセスできます。 - StackTrace:
StackTrace
プロパティには、エラーが発生した場所を判断するために使用できるスタック トレースが含まれています。 - TargetSite:
TargetSite
プロパティを使って、現在の例外をスローしたメソッドを取得できます。
このような例外のプロパティ、基底クラス、継承について確認して、少し圧倒されたように感じても大丈夫です。 例外や例外のプロパティがどのように動作するかの説明よりも、実際のコードで例外をキャッチし、例外のプロパティにアクセスする方が簡単なので安心してください。
注意
このモジュールでは、例外のメッセージ プロパティを使って、アプリケーションのユーザー インターフェイスで例外を報告することに焦点を当てます。
例外オブジェクトのプロパティにアクセスする
例外オブジェクトとそのプロパティを理解したら、コーディングを始めましょう。
Program.cs ファイルを次のように更新します。
C#try { Process1(); } catch { Console.WriteLine("An exception has occurred"); } Console.WriteLine("Exit program"); static void Process1() { try { WriteMessage(); } catch { Console.WriteLine("Exception caught in Process1"); } } static void WriteMessage() { double float1 = 3000.0; double float2 = 0.0; int number1 = 3000; int number2 = 0; Console.WriteLine(float1 / float2); Console.WriteLine(number1 / number2); }
少し時間をかけてコードを確認してください。
これは、前のユニット (チャレンジ アクティビティの解決方法コード) で見たものと同じコードです。
WriteMessage
メソッドの実行時に例外がスローされることはわかっています。 また、Process1
メソッドで例外がキャッチされることもわかっています。 このコードを使って、例外オブジェクトと特定の例外の種類を調べます。次のように、
Process1
メソッドを更新します。C#static void Process1() { try { WriteMessage(); } catch (Exception ex) { Console.WriteLine($"Exception caught in Process1: {ex.Message}"); } }
時間を取って更新内容を確認してください。
更新された
catch
句は、ex
というオブジェクトでException
クラスのインスタンスをキャッチしていることに注目してください。 また、Console.WriteLine()
メソッドがex
を使ってオブジェクトのMessage
プロパティにアクセスし、コンソールにエラー メッセージを表示していることにも注目してください。catch
句は引数なしで使うこともできますが、その方法はお勧めしません。 引数を指定しない場合、すべての例外の種類がキャッチされ、それらを区別することはできません。一般的に、自分のコードが回復方法を認識している例外のみをキャッチするようにします。 そのため、
catch
句には、System.Exception
から派生したオブジェクト引数を指定するようにします。 例外の種類はできるだけ具体的に指定するようにします。 こうすることで、例外ハンドラーが解決できない例外をキャッチすることを回避できます。 この演習の後半では、特定の例外の種類をキャッチするようにコードを更新します。[ファイル] メニューで、 [保存] を選択します。
次のコード行にブレークポイントを設定します。
C#Console.WriteLine($"Exception caught in Process1: {ex.Message}");
[実行] メニューで、[デバッグの開始] を選びます。
コードの実行はブレークポイントで一時停止します。
マウス カーソルを
ex
に合わせます。IntelliSense に、先ほど確認したものと同じ例外のプロパティが表示されることに注目してください。
時間を取って例外オブジェクト
ex
を説明する情報を確認してください。この例外は
System.DivideByZeroException
例外の種類であり、Message
プロパティはAttempted to divide by zero.
に設定されていることに注目してください。StackTrace
プロパティは、エラーが発生したメソッドと行番号と共に、そのエラーの原因となった一連のメソッド呼び出し (と行番号) を報告していることに注目してください。[デバッグ] ツール バーの [続行] を選びます。
時間を取ってコンソール出力を確認してください。
アプリケーションによって生成された出力に例外の
Message
プロパティが含まれていることに注目してください。出力∞ Exception caught in Process1: Attempted to divide by zero. Exit program
特定の例外の種類をキャッチする
キャッチする例外の種類がわかったら、その例外の種類を処理するように catch
句を更新できます。
次のように、
Process1
メソッドを更新します。C#static void Process1() { try { WriteMessage(); } catch (DivideByZeroException ex) { Console.WriteLine($"Exception caught in Process1: {ex.Message}"); } }
コードを保存し、デバッグ セッションを開始します。
更新されたアプリケーションからコンソールには、同じメッセージが報告されることに注目してください。
報告されるメッセージは同じですが、重要な違いがあります。
Process1
メソッドは、処理する準備ができている特定の種類の例外のみをキャッチします。異なる種類の例外を生成するには、
WriteMessage
メソッドを次のように更新します。C#static void WriteMessage() { double float1 = 3000.0; double float2 = 0.0; int number1 = 3000; int number2 = 0; byte smallNumber; Console.WriteLine(float1 / float2); // Console.WriteLine(number1 / number2); checked { smallNumber = (byte)number1; } }
checked
ステートメントの使用に注目してください。ある整数型の値を別の整数型に割り当てる整数型計算を実行する場合、その結果はオーバーフローチェック コンテキストによって変わります。
checked
コンテキストでは、変換元の値が変換先の型の範囲内にあるとき、変換に成功します。 それ以外の場合は、OverflowException
がスローされます。 unchecked コンテキストでは、変換は常に成功し、次のように続行されます。変換元の型が変換先の型より大きい場合、変換元の値はその "余分な" 最上位ビットを破棄することで切り詰められます。 結果は変換先の型の値として扱われます。
変換元の型が変換先の型より小さい場合、変換元の値は変換先の型と同じサイズになるように、符号拡張またはゼロ拡張されます。 変換元の型に符号が付いている場合は符号拡張が利用され、符号が付いていない場合はゼロ拡張が利用されます。 結果は変換先の型の値として扱われます。
変換元の型が変換先の型と同じサイズの場合、変換元の値は変換先の型の値として扱われます。
注意
checked
コード ブロック内にない整数型の計算は、unchecked
コード ブロック内にあるかのように扱われます。コードを保存し、デバッグ セッションを開始します。
新しい例外の種類は、
Process1
メソッド内ではなく、最上位のステートメントのcatch
句によってキャッチされることに注目してください。このアプリケーションからコンソールに次のメッセージが出力されます。
出力∞ An exception has occurred Exit program
注意
Process1
内のcatch
ブロックは実行されません。 これが目的の動作です。 コードが処理する準備ができている例外のみをキャッチします。
コード ブロック内の複数の例外をキャッチする
この時点で、1 つのコード ブロック内で複数の例外が発生した場合はどうなるかが気になるかもしれません。 このコードは、例外が発生するたびに catch
するでしょうか?
次のように、
WriteMessage
メソッドを更新します。C#static void WriteMessage() { double float1 = 3000.0; double float2 = 0.0; int number1 = 3000; int number2 = 0; byte smallNumber; Console.WriteLine(float1 / float2); Console.WriteLine(number1 / number2); checked { smallNumber = (byte)number1; } }
次のコード行の
WriteMessage()
メソッド内にブレークポイントを設定します。C#Console.WriteLine(float1 / float2);
コードを保存し、デバッグ セッションを開始します。
コードを 1 行ずつステップ実行し、コードが最初の例外を処理した後に何が起こるかを確認します。
最初の例外が発生すると、例外を処理できる最初の
catch
句に制御が渡されます。 2 つ目の例外を生成するコードには到達しません。 つまり、コードの一部は実行されません。 これは深刻な問題につながる可能性があります。複数の例外を管理する方法と、複数の例外を管理しない場合とその理由について、時間を取って考えてください。
このモジュールの前半では、例外はできるだけ発生した場所の近くでキャッチすべきであることを学習しました。 このことを念頭に置いて、独自の
try-catch
を使って例外をキャッチするようにWriteMessage
メソッドを更新することを選択できます。 次に例を示します。C#static void WriteMessage() { double float1 = 3000.0; double float2 = 0.0; int number1 = 3000; int number2 = 0; byte smallNumber; try { Console.WriteLine(float1 / float2); Console.WriteLine(number1 / number2); } catch (DivideByZeroException ex) { Console.WriteLine($"Exception caught in WriteMessage: {ex.Message}"); } checked { smallNumber = (byte)number1; } }
また、
OverflowException
を引き起こすコードをWriteMessage()
メソッド内の別のtry-catch
でラップすることもできます。C#checked { try { smallNumber = (byte)number1; } catch (OverflowException ex) { Console.WriteLine($"Exception caught in WriteMessage: {ex.Message}"); } }
後続の例外をキャッチすることが望ましくないのは、どのような状況ですか?
メソッド (またはコード ブロック) が 2 つの部分からなるプロセスを完了する場合を考えてみましょう。 プロセスの 2 つ目の部分は、1 つ目の部分が完了することに依存しているとします。 1 つ目のプロセスが正常に完了しない場合、2 つ目のプロセスに進む意味はありません。 このような場合、残りの部分または大規模なプロセスの一部を試行するのではなく、ユーザーにエラー状態を説明するメッセージを表示する方がよいことがよくあります。
コード ブロックで別の例外の種類をキャッチする
データ型の違いによって、さまざまな例外が発生する場合があります。
ブレークポイントをクリアし、Program.cs ファイルの内容を次のコードに置き換えます。
C#// inputValues is used to store numeric values entered by a user string[] inputValues = new string[]{"three", "9999999999", "0", "2" }; foreach (string inputValue in inputValues) { int numValue = 0; try { numValue = int.Parse(inputValue); } catch (FormatException) { Console.WriteLine("Invalid readResult. Please enter a valid number."); } catch (OverflowException) { Console.WriteLine("The number you entered is too large or too small."); } catch(Exception ex) { Console.WriteLine(ex.Message); } }
時間を取ってこのコードを確認してください。
まず、このコードによって
inputValues
という文字列配列が作成されます。 この配列のデータは、数値の入力を指示されたユーザーが入力した値を表すためのものです。 入力された値によって、さまざまな例外の種類が発生する可能性があります。このコードでは、
int.Parse
メソッドを使って文字列の "入力" 値を整数に変換していることに注目してください。int.Parse
のコードはtry
コード ブロック内に配置されています。次のコード行にブレークポイントを設定します。
C#int numValue = 0;
コードを保存し、デバッグ セッションを開始します。
コードを 1 行ずつステップ実行し、さまざまな例外の種類がキャッチされていることを確認します。
要点
このユニットで覚えておく必要があるいくつかの重要な点を次に示します。
catch
句は、特定の例外の種類をキャッチするように構成します。 たとえば、DivideByZeroException
例外の種類です。- 例外オブジェクトのプロパティは、
catch
ブロック内でアクセスできます。 たとえば、Message
プロパティを使ってアプリケーション ユーザーに問題を通知することができます。 - 複数の例外の種類をキャッチする必要がある場合は、複数の
catch
句を指定できます。