简介

软件程序员通过编写代码实现预期功能,但每个软件都可能因内部或外部原因未能执行其预期操作。C# 的异常处理系统允许程序员以结构化的方式处理错误或异常情况,从而将正常的代码流程与错误处理逻辑分离。

异常可以表示软件执行过程中出现的多种异常情况,这些异常可能是由内部或外部原因引起的:

  • 外部原因:如网络连接失败、文件或系统资源访问权限不足、内存不足、或 Web 服务抛出的异常。这些通常是由应用程序依赖的外部环境(如操作系统、.NET 运行时、外部应用程序或组件)引起的。
  • 内部原因:如软件缺陷、业务规则设计的功能性失败(如账户余额不足时禁止提现)、或由外部原因传播的异常(如运行时检测到的空对象引用、应用程序代码检测到的无效输入字符串)。

检测到错误条件的代码称为抛出异常,而处理这些错误的代码称为捕获异常。在 C# 中,异常是一个对象,封装了与错误相关的多种信息,如异常发生时的堆栈跟踪和描述性错误消息。所有异常对象都是 System.Exception 类或其子类的实例。
.NET Framework 定义了许多异常类用于各种用途,程序员也可以通过继承 System.Exception 或其他适当的异常类来自定义异常类。

在 .NET 2.0 之前,Microsoft 推荐开发人员的异常类继承自 ApplicationException。但在 .NET 2.0 之后,这一建议被废弃,现在用户定义的异常类应直接继承自 Exception 类。


概述

C# 中有三种异常处理的代码定义:

  1. try/catch:执行代码,如果发生错误则捕获异常。
  2. try/catch/finally:执行代码,捕获异常(如果发生),但无论是否发生异常都执行 finally 块。
  3. try/finally:执行代码,无论是否发生异常都执行 finally 块。若有异常发生,会在 finally 执行完成后抛出。

异常的捕获顺序: 异常按照从最具体最不具体的顺序被捕获。例如,访问不存在的文件会触发以下顺序的异常处理:

  1. FileNotFoundException
  2. IOExceptionFileNotFoundException 的基类)
  3. SystemExceptionIOException 的基类)
  4. ExceptionSystemException 的基类)

如果抛出的异常未派生自捕获列表中的任何异常类,则会沿调用堆栈向上抛出。


示例

以下是几种不同类型异常的示例:

示例代码:try/catchtry/catch/finally


try/catch 示例

try/catch 语句用于尝试执行某些操作,如果发生错误,将控制转移到匹配的 catch 块。

示例 1:捕获单一异常

using System;

class ExceptionTest
{
     public static void Main(string[] args)
     {
          try
          {
               // 尝试访问数组中的元素
               Console.WriteLine(args[0]);
               Console.WriteLine(args[1]);
               Console.WriteLine(args[2]);
               Console.WriteLine(args[3]);
               Console.WriteLine(args[4]);
          }
          catch (ArgumentOutOfRangeException e)
          {
               // 捕获超出范围的异常
               Console.WriteLine(e.Message);
          }
     }
}

示例 2:捕获多种异常

using System;
using System.IO;

class ExceptionTest
{
     public static void Main(string[] args)
     {
          try
          {
               // 读取文件内容
               string fileContents = new StreamReader(@"C:\log.txt").ReadToEnd();
          }
          catch (UnauthorizedAccessException e) // 文件访问权限问题
          {
               Console.WriteLine(e.Message);
          }
          catch (FileNotFoundException e)       // 文件不存在
          {
               Console.WriteLine(e.Message);
          }
          catch (IOException e)                // 其他 IO 问题
          {
               Console.WriteLine(e.Message);
          }
     }
}

示例 3:省略异常类型和变量名

catch 块中可以省略异常的类型和变量名:

try
{
    int number = 1 / 0; // 除以零异常
}
catch (DivideByZeroException)
{
    Console.WriteLine("Cannot divide by zero.");
}
catch
{
    Console.WriteLine("An unspecified exception occurred.");
}

try/catch/finally 示例

当异常可能使程序处于无效状态时,可以使用 finally 块来确保某些操作(如释放资源)始终被执行,无论是否发生异常。

示例:处理数据库连接

using System;
using System.Data;
using System.Data.SqlClient;

class ExceptionTest
{
     public static void Main(string[] args)
     {
          SqlConnection sqlConn = null;

          try
          {
              // 创建数据库连接
              sqlConn = new SqlConnection(/* 连接字符串 */);
              sqlConn.Open();
 
              // 执行数据库操作
              Console.WriteLine("Database operations executed.");
          }
          catch (SqlException e)
          {
               // 捕获 SQL 异常
               Console.WriteLine($"SQL Error: {e.Message}");
          }
          finally
          {
               // 确保连接被正确释放
               if (sqlConn != null && sqlConn.State != ConnectionState.Closed)
               {
                   sqlConn.Dispose();
                   Console.WriteLine("Database connection closed.");
               }
          }
     }
}

说明:

  • try 块中打开数据库连接并进行操作。
  • 如果发生异常,catch 块会捕获并处理异常。
  • 无论是否发生异常,finally 块都会确保释放数据库连接资源。

finally 块用于确保清理操作(如关闭文件、释放内存或关闭数据库连接)始终得以执行,从而避免资源泄漏或不一致的状态。

第二个示例:异常处理代码

以下代码展示了一个简单的异常处理示例,其中包括多个 catch 块、finally 块,以及如何正确地捕获和处理异常。

代码示例 1:捕获多个异常并使用 finally

using System;

public class Excepation
{
    public double num1, num2, result;

    public void Add()
    {
        try
        {
            Console.WriteLine("Enter your numbers:");
            num1 = Convert.ToInt32(Console.ReadLine());
            num2 = Convert.ToInt32(Console.ReadLine());
            result = num1 / num2; // 可能触发 DivideByZeroException
        }
        catch (DivideByZeroException e)
        {
            Console.WriteLine("Error: {0}", e.Message); // 捕获除以零异常
        }
        catch (FormatException ex)
        {
            Console.WriteLine("Error: {0}", ex.Message); // 捕获格式错误异常
        }
        finally
        {
            Console.WriteLine("Operation complete.");
        }
    }

    public void Display()
    {
        Console.WriteLine("The Result is: {0}", result);
    }

    public static void Main()
    {
        Excepation ex = new Excepation();
        ex.Add();
        ex.Display();
    }
}

说明:

  • try 块尝试从用户输入获取两个数字并执行除法。
  • catch 块分别捕获 DivideByZeroExceptionFormatException
  • finally 块始终会执行,用于打印操作完成信息。

代码示例 2:try/finally

try/finally 的主要用途是确保资源释放或其他清理操作。以下示例展示如何管理数据库连接:

using System;
using System.Data;
using System.Data.SqlClient;

class ExceptionTest
{
    public static void Main(string[] args)
    {
        SqlConnection sqlConn = null;

        try
        {
            sqlConn = new SqlConnection(/* Connection string */);
            sqlConn.Open();
            // 执行数据库操作
        }
        finally
        {
            if (sqlConn != null && sqlConn.State != ConnectionState.Closed)
            {
                sqlConn.Dispose(); // 确保连接被释放
            }
        }
    }
}

说明:

  • 即使 try 块中发生异常,finally 块仍会确保连接被正确关闭。
  • 不在 try 块中声明资源,以便 finally 块可以访问。

重新抛出异常

有时,抛出异常更有意义,例如:

  • 异常超出了预期范围。
  • 需要向异常中添加额外信息以帮助诊断。

错误示例:不要使用 throw ex

try
{
    // 执行操作
}
catch (Exception ex)
{
    throw ex; // 不推荐
}

推荐示例:使用 throw 保留原始异常信息

try
{
    string value = ConfigurationManager.AppSettings["Timeout"];
    if (value == null)
        throw new ConfigurationErrorsException("Timeout value is not in the configuration file.");
}
catch (Exception ex)
{
    throw; // 保留原始异常信息并向上抛出
}

添加额外信息到异常中

有时,可以通过嵌套异常添加额外信息。以下示例展示如何将现有异常作为 InnerException 传递:

public OrderItem LoadItem(string itemNumber)
{
    DataTable dt = null;

    try
    {
        if (itemNumber == null)
            throw new ArgumentNullException("Item number cannot be null", "itemNumber");

        dt = DataAccess.OrderItem.Load(itemNumber);
        if (dt.Rows.Count == 0)
            return null;
        else if (dt.Rows.Count > 1)
            throw new DuplicateDataException("Multiple items map to this item.", itemNumber, dt);

        OrderItem item = OrderItem.CreateInstanceFromDataRow(dt.Rows[0]);
        if (item == null)
            throw new ErrorLoadingException("Error loading item " + itemNumber, itemNumber, dt.Rows[0]);
    }
    catch (DuplicateDataException dde)
    {
        throw new ErrorLoadingException("OrderItem.LoadItem failed with item " + itemNumber, dde);
    }
    catch (Exception ex)
    {
        throw; // 其他异常直接向上抛出
    }
}

说明:

  • 捕获 DuplicateDataException 并将其嵌套到 ErrorLoadingException 中。
  • 对于未预期的异常,直接重新抛出。

总结

  1. 始终捕获预期的异常,不要捕获和忽略所有异常。
  2. 使用 finally 确保资源被正确释放。
  3. 使用 throw 保留原始异常信息。
  4. 在需要时,嵌套异常并添加额外信息以帮助诊断问题。
最后修改: 2025年01月11日 星期六 22:01