练习 - 捕获特定异常类型

已完成

在本模块的前面部分,你了解到 C# 应用程序捕获的异常对象是异常类的实例。 一般来说,代码将 catch 以下异常之一:

  • 作为基类实例的 System.Exception 异常对象。
  • 异常对象,它是从基类继承的异常类型的实例。 例如,InvalidCastException 类的一个实例。

检查异常属性

System.Exception 是所有派生异常类型继承自的基类。 每个异常类型都通过特定的类层次结构从基类继承。 例如,类 InvalidCastException 层次结构如下所示:

Object
    Exception
        SystemException
            InvalidCastException

Exception 继承的大多数异常类不会添加任何其他功能;它们只是继承自 Exception。 因此,通过检查类的属性 Exception ,可以了解大多数异常,以及如何在代码中使用异常。

下面是类的属性 Exception

  • 数据:该 Data 属性在键值对中保存任意数据。
  • HelpLinkHelpLink 属性可用于保存包含 URL(或 URN)的帮助文件,提供有关异常原因的详细信息。
  • HResult:该 HResult 属性可用于访问分配给特定异常的编码数值。
  • InnerException:属性 InnerException 可用于在异常处理期间创建和保留一系列异常。
  • 消息:该 Message 属性提供有关异常原因的详细信息。
  • :该 Source 属性可用于访问应用程序的名称或导致错误的对象。
  • StackTrace:该 StackTrace 属性包含可用于确定发生错误的堆栈跟踪。
  • TargetSite:该 TargetSite 属性可用于获取引发当前异常的方法。

如果你对异常属性、基类和继承的检查感到有点不知所措,没关系。 别担心,在代码中捕获异常并访问异常的属性比解释异常和异常属性的工作原理更容易。

注释

在本模块中,你将重点介绍如何使用异常的消息属性在应用程序的用户界面中报告异常。

访问异常对象的属性

了解异常对象及其属性后,就可以开始编码了。

  1. 按如下所示更新Program.cs文件:

    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 方法,如下所示:

    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. 在以下代码行上设置断点:

    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 方法,如下所示:

    static void Process1()
    {
        try
        {
            WriteMessage();
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine($"Exception caught in Process1: {ex.Message}");
        }
    }
    
  2. 保存代码,然后启动调试会话。

  3. 请注意,更新的应用程序向控制台报告相同的消息。

    虽然报告的消息相同,但有一个重要区别。 您的Process1方法将仅捕获它专门准备处理的特定类型异常。

  4. 若要生成其他异常类型,请按如下所示更新 WriteMessage 方法:

    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。 在不受限制的上下文中,转换总是成功的,具体过程如下:

    • 如果源类型大于目标类型,则通过放弃其“额外”最重要的位来截断源值。 结果会被视为目标类型的值。

    • 如果源类型小于目标类型,则源值为符号扩展或零扩展,使其大小与目标类型相同。 如果源类型已签名,则使用签名扩展;如果源类型未签名,则使用零扩展。 结果会被视为目标类型的值。

    • 如果源类型的大小与目标类型相同,则源值被视为目标类型的值。

    注释

    如果整型计算不在checked代码块内,则将其视为在unchecked代码块内进行。

  6. 保存代码,然后启动调试会话。

  7. 请注意,新的异常类型由顶级语句中的 catch 子句捕获,而不是在 Process1 方法中捕获。

    应用程序将以下消息输出到控制台:

    ∞
    An exception has occurred
    Exit program
    

    注释

    位于Process1中的catch块未被执行。 这是你想要的行为。 只捕获代码准备处理的异常。

捕获代码块中的多个异常

此时,你可能想知道在单个代码块中出现多个异常时会发生什么情况。 代码是否会在发生异常时 catch 每个异常?

  1. 更新 WriteMessage 方法,如下所示:

    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() 方法内的断点:

    Console.WriteLine(float1 / float2);
    
  3. 保存代码,然后启动调试会话。

  4. 逐行执行代码,并注意代码处理第一个异常后会发生什么情况。

    发生第一个异常时,控件将传递给可以处理异常的第一个 catch 子句。 生成第二个异常的代码永远不会被执行。 这意味着你的一些代码从未被执行。 这可能导致严重的问题。

  5. 花一分钟时间考虑如何管理多个异常,以及何时/为什么不希望代码管理多个异常。

    你之前在本模块中了解到,异常应尽可能在发生的位置附近被捕捉。 考虑到这一点,可以选择更新 WriteMessage 方法以使用它自己的 try-catch 来捕获异常。 例如:

    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 中。

    checked
    {
        try
        {
            smallNumber = (byte)number1;
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"Exception caught in WriteMessage: {ex.Message}");
        }  
    }
    
  6. 在什么情况下,捕获后续异常是不可取的?

    假设方法(或代码块)正在完成两个部分过程。 假设该过程的第二部分依赖于第一部分完成。 如果进程的第一部分无法成功完成,则无法继续执行该过程的第二部分。 在这种情况下,最好向用户显示解释错误条件的消息,而无需尝试较大进程的剩余部分或部分。

在代码块中捕获单独的异常类型

有时,数据中的变体可能会导致不同类型的异常。

  1. 清除断点,然后将Program.cs文件的内容替换为以下代码:

    // 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 该方法将字符串“input”值转换为整数。 int.Parse 代码放置在 try 代码块内。

  3. 在以下代码行上设置断点:

    int numValue = 0;
    
  4. 保存代码,然后启动调试会话。

  5. 逐行逐行执行代码,并注意到捕获了不同的异常类型。

回顾

在本单元中,应谨记以下几个重要事项:

  • 应将 catch 子句配置为捕获特定的异常类型。 例如, DivideByZeroException 异常类型。
  • 可以在catch块中访问异常对象的属性。 例如,可以使用该 Message 属性通知应用程序用户出现问题。
  • 如果需要捕获多个异常类型,可以指定两个或多个 catch 子句。