演習 - 特定の例外の種類をキャッチする

完了 100 XP

このモジュールの前半で、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 プロパティを使って、現在の例外をスローしたメソッドを取得できます。

このような例外のプロパティ、基底クラス、継承について確認して、少し圧倒されたように感じても大丈夫です。 例外や例外のプロパティがどのように動作するかの説明よりも、実際のコードで例外をキャッチし、例外のプロパティにアクセスする方が簡単なので安心してください。

注意

このモジュールでは、例外のメッセージ プロパティを使って、アプリケーションのユーザー インターフェイスで例外を報告することに焦点を当てます。

例外オブジェクトのプロパティにアクセスする

例外オブジェクトとそのプロパティを理解したら、コーディングを始めましょう。

  1. 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);
    }
    
  2. 少し時間をかけてコードを確認してください。

    これは、前のユニット (チャレンジ アクティビティの解決方法コード) で見たものと同じコードです。 WriteMessage メソッドの実行時に例外がスローされることはわかっています。 また、Process1 メソッドで例外がキャッチされることもわかっています。 このコードを使って、例外オブジェクトと特定の例外の種類を調べます。

  3. 次のように、Process1 メソッドを更新します。

    C#
    static void Process1()
    {
        try
        {
            WriteMessage();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception caught in Process1: {ex.Message}");
        }
    }
    
  4. 時間を取って更新内容を確認してください。

    更新された catch 句は、ex というオブジェクトで Exception クラスのインスタンスをキャッチしていることに注目してください。 また、Console.WriteLine() メソッドが ex を使ってオブジェクトの Message プロパティにアクセスし、コンソールにエラー メッセージを表示していることにも注目してください。

    catch 句は引数なしで使うこともできますが、その方法はお勧めしません。 引数を指定しない場合、すべての例外の種類がキャッチされ、それらを区別することはできません。

    一般的に、自分のコードが回復方法を認識している例外のみをキャッチするようにします。 そのため、catch 句には、System.Exception から派生したオブジェクト引数を指定するようにします。 例外の種類はできるだけ具体的に指定するようにします。 こうすることで、例外ハンドラーが解決できない例外をキャッチすることを回避できます。 この演習の後半では、特定の例外の種類をキャッチするようにコードを更新します。

  5. [ファイル] メニューで、 [保存] を選択します。

  6. 次のコード行にブレークポイントを設定します。

    C#
    Console.WriteLine($"Exception caught in Process1: {ex.Message}");
    
  7. [実行] メニューで、[デバッグの開始] を選びます。

    コードの実行はブレークポイントで一時停止します。

  8. マウス カーソルを ex に合わせます。

    IntelliSense に、先ほど確認したものと同じ例外のプロパティが表示されることに注目してください。

  9. 時間を取って例外オブジェクト ex を説明する情報を確認してください。

    この例外は System.DivideByZeroException 例外の種類であり、Message プロパティは Attempted to divide by zero. に設定されていることに注目してください。

    StackTrace プロパティは、エラーが発生したメソッドと行番号と共に、そのエラーの原因となった一連のメソッド呼び出し (と行番号) を報告していることに注目してください。

  10. [デバッグ] ツール バー[続行] を選びます。

  11. 時間を取ってコンソール出力を確認してください。

    アプリケーションによって生成された出力に例外の Message プロパティが含まれていることに注目してください。

    出力
    ∞
    Exception caught in Process1: Attempted to divide by zero.
    Exit program
    

特定の例外の種類をキャッチする

キャッチする例外の種類がわかったら、その例外の種類を処理するように catch 句を更新できます。

  1. 次のように、Process1 メソッドを更新します。

    C#
    static void Process1()
    {
        try
        {
            WriteMessage();
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine($"Exception caught in Process1: {ex.Message}");
        }
    }
    
  2. コードを保存し、デバッグ セッションを開始します。

  3. 更新されたアプリケーションからコンソールには、同じメッセージが報告されることに注目してください。

    報告されるメッセージは同じですが、重要な違いがあります。 Process1 メソッドは、処理する準備ができている特定の種類の例外のみをキャッチします。

  4. 異なる種類の例外を生成するには、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;
        }
    }
    
  5. checked ステートメントの使用に注目してください。

    ある整数型の値を別の整数型に割り当てる整数型計算を実行する場合、その結果はオーバーフローチェック コンテキストによって変わります。 checked コンテキストでは、変換元の値が変換先の型の範囲内にあるとき、変換に成功します。 それ以外の場合は、OverflowException がスローされます。 unchecked コンテキストでは、変換は常に成功し、次のように続行されます。

    • 変換元の型が変換先の型より大きい場合、変換元の値はその "余分な" 最上位ビットを破棄することで切り詰められます。 結果は変換先の型の値として扱われます。

    • 変換元の型が変換先の型より小さい場合、変換元の値は変換先の型と同じサイズになるように、符号拡張またはゼロ拡張されます。 変換元の型に符号が付いている場合は符号拡張が利用され、符号が付いていない場合はゼロ拡張が利用されます。 結果は変換先の型の値として扱われます。

    • 変換元の型が変換先の型と同じサイズの場合、変換元の値は変換先の型の値として扱われます。

    注意

    checked コード ブロック内にない整数型の計算は、unchecked コード ブロック内にあるかのように扱われます。

  6. コードを保存し、デバッグ セッションを開始します。

  7. 新しい例外の種類は、Process1 メソッド内ではなく、最上位のステートメントの catch 句によってキャッチされることに注目してください。

    このアプリケーションからコンソールに次のメッセージが出力されます。

    出力
    ∞
    An exception has occurred
    Exit program
    

    注意

    Process1 内の catch ブロックは実行されません。 これが目的の動作です。 コードが処理する準備ができている例外のみをキャッチします。

コード ブロック内の複数の例外をキャッチする

この時点で、1 つのコード ブロック内で複数の例外が発生した場合はどうなるかが気になるかもしれません。 このコードは、例外が発生するたびに catch するでしょうか?

  1. 次のように、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;
        }
    }
    
  2. 次のコード行の WriteMessage() メソッド内にブレークポイントを設定します。

    C#
    Console.WriteLine(float1 / float2);
    
  3. コードを保存し、デバッグ セッションを開始します。

  4. コードを 1 行ずつステップ実行し、コードが最初の例外を処理した後に何が起こるかを確認します。

    最初の例外が発生すると、例外を処理できる最初の catch 句に制御が渡されます。 2 つ目の例外を生成するコードには到達しません。 つまり、コードの一部は実行されません。 これは深刻な問題につながる可能性があります。

  5. 複数の例外を管理する方法と、複数の例外を管理しない場合とその理由について、時間を取って考えてください。

    このモジュールの前半では、例外はできるだけ発生した場所の近くでキャッチすべきであることを学習しました。 このことを念頭に置いて、独自の 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}");
        }  
    }
    
  6. 後続の例外をキャッチすることが望ましくないのは、どのような状況ですか?

    メソッド (またはコード ブロック) が 2 つの部分からなるプロセスを完了する場合を考えてみましょう。 プロセスの 2 つ目の部分は、1 つ目の部分が完了することに依存しているとします。 1 つ目のプロセスが正常に完了しない場合、2 つ目のプロセスに進む意味はありません。 このような場合、残りの部分または大規模なプロセスの一部を試行するのではなく、ユーザーにエラー状態を説明するメッセージを表示する方がよいことがよくあります。

コード ブロックで別の例外の種類をキャッチする

データ型の違いによって、さまざまな例外が発生する場合があります。

  1. ブレークポイントをクリアし、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);
        }
    }
    
  2. 時間を取ってこのコードを確認してください。

    まず、このコードによって inputValues という文字列配列が作成されます。 この配列のデータは、数値の入力を指示されたユーザーが入力した値を表すためのものです。 入力された値によって、さまざまな例外の種類が発生する可能性があります。

    このコードでは、int.Parse メソッドを使って文字列の "入力" 値を整数に変換していることに注目してください。 int.Parse のコードは try コード ブロック内に配置されています。

  3. 次のコード行にブレークポイントを設定します。

    C#
    int numValue = 0;
    
  4. コードを保存し、デバッグ セッションを開始します。

  5. コードを 1 行ずつステップ実行し、さまざまな例外の種類がキャッチされていることを確認します。

要点

このユニットで覚えておく必要があるいくつかの重要な点を次に示します。

  • catch 句は、特定の例外の種類をキャッチするように構成します。 たとえば、DivideByZeroException 例外の種類です。
  • 例外オブジェクトのプロパティは、catch ブロック内でアクセスできます。 たとえば、Message プロパティを使ってアプリケーション ユーザーに問題を通知することができます。
  • 複数の例外の種類をキャッチする必要がある場合は、複数の catch 句を指定できます。

次のユニット: 演習 - チャレンジ アクティビティを完了して特定の例外をキャッチする

前へ 次へ