Skip to content

06-异常捕获 ​

异常是指程序在运行时发生的错误或意外情况。C# 提供了完善的异常处理机制,使得程序在遇到错误时可以优雅地处理,而不是直接崩溃。


一、编译错误 vs 运行错误 ​

对比项编译错误运行错误(异常)
发生时间编译阶段程序运行时
原因语法错误、类型不匹配等逻辑错误、环境问题等
影响无法生成可执行文件程序可能崩溃
处理方式编译器提示,修正代码try-catch 捕获或修正代码
示例缺少分号、变量未声明除零、空引用、数组越界
csharp
// 编译错误——编译时发现,无法运行
// int number = "Hello";  // 类型不匹配

// 运行错误——编译通过,运行时崩溃
int divisor = 0;
// int result = 10 / divisor;  // DivideByZeroException

二、.NET 常用异常类型 ​

异常类型说明触发场景
System.Exception所有异常的基类通用的异常捕获
System.SystemException系统抛出的异常基类—
System.NullReferenceException空引用异常调用了 null 对象的成员
System.IndexOutOfRangeException索引越界数组/集合索引超出范围
System.DivideByZeroException除零异常整数除以 0
System.ArgumentException参数异常方法参数不合法
System.ArgumentNullException参数为空异常null 参数不允许时
System.ArgumentOutOfRangeException参数超出范围索引/值超出允许范围
System.FormatException格式异常字符串格式不匹配
System.InvalidCastException无效转换异常类型转换失败
System.InvalidOperationException无效操作异常对象状态不允许操作
System.OverflowException溢出异常算术运算溢出
System.IO.IOExceptionIO 异常文件读写错误
System.IO.FileNotFoundException文件未找到文件不存在
System.UnauthorizedAccessException访问权限异常没有权限访问资源

三、异常捕获机制 ​

1. 基本结构:try-catch-finally ​

csharp
try
{
    // 可能抛出异常的代码
    int result = 10 / 0;
}
catch (Exception ex)
{
    // 处理异常
    Console.WriteLine($"发生异常:{ex.Message}");
    Console.WriteLine($"异常类型:{ex.GetType().Name}");
    Console.WriteLine($"堆栈跟踪:{ex.StackTrace}");
}
finally
{
    // 无论是否异常都会执行(可选)
    Console.WriteLine("清理资源");
}

2. 捕获特定异常 ​

csharp
try
{
    string? str = null;
    Console.WriteLine(str.Length);  // NullReferenceException
}
catch (NullReferenceException ex)
{
    Console.WriteLine($"空引用:{ex.Message}");
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"除零:{ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"其他异常:{ex.Message}");
}

注意: 异常捕获按顺序匹配,应将更具体的异常写在前面,通用的 Exception 写在最后。

3. 异常过滤器(when 关键字,C# 6.0+) ​

csharp
try
{
    // 可能抛出异常的代码
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    Console.WriteLine("404 错误");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.InternalServerError)
{
    Console.WriteLine("500 错误");
}
catch (Exception ex) when (Log(ex))  // 只记录不捕获
{
    // 这里不会执行,因为 Log 返回 false
}

static bool Log(Exception ex)
{
    Console.WriteLine($"记录异常:{ex.Message}");
    return false;  // 返回 false 表示不捕获
}

4. finally 块 ​

csharp
// finally 中的代码总会执行——无论是否异常,无论是否有 return
static string TestFinally()
{
    try
    {
        Console.WriteLine("try 执行");
        return "返回值";
    }
    catch (Exception ex)
    {
        Console.WriteLine($"catch 执行:{ex.Message}");
        return "异常返回值";
    }
    finally
    {
        Console.WriteLine("finally 执行——即使在 return 之后");
    }
}

// finally 常用于释放资源
FileStream? file = null;
try
{
    file = File.OpenRead(@"C:\test.txt");
    // 处理文件...
}
catch (FileNotFoundException)
{
    Console.WriteLine("文件未找到");
}
finally
{
    file?.Close();  // 确保文件被关闭
}

5. using 语句——try-finally 的语法糖 ​

csharp
// 以下两种写法等价:

// 写法一:传统 try-finally
FileStream fs1 = new FileStream(@"C:\test.txt", FileMode.Open);
try
{
    // 使用文件...
}
finally
{
    fs1.Dispose();
}

// 写法二:using 语句(推荐)
using (FileStream fs2 = new FileStream(@"C:\test.txt", FileMode.Open))
{
    // 使用文件...
}  // 自动调用 Dispose()

// 写法三:using 声明(C# 8.0+)
using FileStream fs3 = new FileStream(@"C:\test.txt", FileMode.Open);
// 使用文件...
// 作用域结束时自动释放

四、抛出异常 ​

1. throw 语句 ​

csharp
static void ValidateAge(int age)
{
    if (age < 0)
        throw new ArgumentOutOfRangeException(nameof(age), "年龄不能为负数");

    if (age > 150)
        throw new ArgumentException("年龄超出合理范围", nameof(age));
}

// 使用
try
{
    ValidateAge(-5);
}
catch (ArgumentException ex)
{
    Console.WriteLine($"参数错误:{ex.Message}");
}

2. 异常再抛出 ​

csharp
try
{
    // 某些操作
}
catch (Exception ex)
{
    // 记录日志后重新抛出

    // ✅ 正确:保留原始堆栈信息
    throw;

    // ❌ 错误:重置堆栈信息,丢失原始调用链
    // throw ex;

    // ✅ 包装为新的异常(保留原始异常作为 InnerException)
    // throw new ApplicationException("发生错误", ex);
}

五、自定义异常 ​

csharp
// 自定义异常类需要继承 Exception
public class OrderException : Exception
{
    // 默认构造函数
    public OrderException() : base("订单处理错误") { }

    // 带消息
    public OrderException(string message) : base(message) { }

    // 带消息和内部异常
    public OrderException(string message, Exception inner) : base(message, inner) { }

    // 自定义属性
    public string OrderId { get; set; }
}

// 使用自定义异常
void ProcessOrder(string orderId)
{
    try
    {
        // 处理订单...
        throw new InvalidOperationException("库存不足");
    }
    catch (Exception ex)
    {
        throw new OrderException($"订单 {orderId} 处理失败")
        {
            OrderId = orderId
        };
    }
}

// 捕获自定义异常
try
{
    ProcessOrder("ORD-001");
}
catch (OrderException ex)
{
    Console.WriteLine($"订单错误:{ex.Message},订单号:{ex.OrderId}");
}

六、InnerException ​

当在 catch 块中抛出新异常时,可将原始异常作为内部异常传递。

csharp
try
{
    try
    {
        int.Parse("invalid");  // 原始异常:FormatException
    }
    catch (FormatException ex)
    {
        // 包装为新异常,保留原始异常信息
        throw new InvalidOperationException("处理数据时出错", ex);
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"消息:{ex.Message}");
    Console.WriteLine($"内部异常:{ex.InnerException?.GetType().Name}");
    Console.WriteLine($"内部异常消息:{ex.InnerException?.Message}");
}

七、异常处理最佳实践 ​

1. 在合适的层级捕获异常 ​

csharp
// ❌ 不要在每个方法中都 catch——太冗长
void Method1()
{
    try { /* ... */ }
    catch (Exception ex) { Log(ex); throw; }
}

// ✅ 在顶层统一处理
void TopLevel()
{
    try
    {
        Method1();
    }
    catch (Exception ex)
    {
        Log(ex);
        ShowError(ex.Message);
    }
}

2. 捕获特定异常而非 Exception ​

csharp
// ❌ 不推荐:捕获所有异常
try { /* ... */ }
catch (Exception ex) { /* 处理 */ }

// ✅ 推荐:只捕获你知道如何处理的异常
try { /* ... */ }
catch (FileNotFoundException ex) { /* 文件不存在 */ }
catch (UnauthorizedAccessException ex) { /* 权限不足 */ }

3. 避免静默吞异常 ​

csharp
// ❌ 危险:吞掉异常,程序可能处于不一致状态
try { /* ... */ }
catch (Exception) { /* 什么都不做 */ }

// ✅ 至少记录日志
try { /* ... */ }
catch (Exception ex)
{
    Logger.Log(ex);
    // 然后根据情况决定:重新抛出 / 返回默认值 / 提示用户
}

4. 使用 finally 释放资源 ​

csharp
// 使用 using 或 finally 确保资源释放
using var connection = new SqlConnection(connectionString);
// 使用连接...
// 自动释放

5. 不要让异常影响控制流 ​

csharp
// ❌ 不推荐:用异常控制流程
try
{
    int.Parse(userInput);
}
catch (FormatException)
{
    Console.WriteLine("无效输入");
}

// ✅ 推荐:用 TryParse 避免异常
if (int.TryParse(userInput, out int result))
{
    Console.WriteLine($"有效数字:{result}");
}
else
{
    Console.WriteLine("无效输入");
}

八、综合案例 ​

csharp
// 用户注册案例——展示异常处理的完整使用
public class UserRegistration
{
    public class RegistrationException : Exception
    {
        public RegistrationException(string message, Exception? inner = null)
            : base(message, inner) { }
    }

    public void Register(string username, string email, int age)
    {
        // 参数验证
        if (string.IsNullOrWhiteSpace(username))
            throw new ArgumentException("用户名不能为空", nameof(username));

        if (!email.Contains("@"))
            throw new ArgumentException("邮箱格式无效", nameof(email));

        if (age < 0 || age > 150)
            throw new ArgumentOutOfRangeException(nameof(age), "年龄不合法");

        try
        {
            // 模拟数据库操作
            SaveToDatabase(username, email, age);
            Console.WriteLine("注册成功");
        }
        catch (SqlException ex) when (ex.Number == 2627)  // 主键冲突
        {
            throw new RegistrationException("用户名已存在", ex);
        }
        catch (IOException ex)
        {
            throw new RegistrationException("数据库连接失败,请稍后重试", ex);
        }
        finally
        {
            Console.WriteLine("注册操作结束");
        }
    }

    private void SaveToDatabase(string username, string email, int age)
    {
        // 模拟可能的异常
        throw new IOException("数据库连接超时");
    }
}

// 使用
try
{
    var registration = new UserRegistration();
    registration.Register("张三", "invalid-email", 25);
}
catch (ArgumentException ex)
{
    Console.WriteLine($"输入错误:{ex.Message}");
}
catch (UserRegistration.RegistrationException ex)
{
    Console.WriteLine($"注册失败:{ex.Message}");
    if (ex.InnerException != null)
        Console.WriteLine($"原因:{ex.InnerException.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"意外错误:{ex.Message}");
}

核心知识点总结 ​

异常处理结构 ​

结构说明是否必需
try包裹可能抛出异常的代码必需
catch捕获并处理特定异常可选(至少一个 catch 或 finally)
finally无论是否异常都执行可选

异常处理原则 ​

原则说明
精确捕获捕获你能处理的特定异常类型
及时清理使用 using 或 finally 释放资源
避免吞异常不要写空的 catch 块
保留堆栈使用 throw;(不是 throw ex;)重新抛出
不要用异常控制流程使用 TryParse 等模式替代
自定义异常继承 Exception,命名以 Exception 结尾
调用链信息通过 InnerException 保留原始异常

注意事项 ​

  1. 异常开销大——不要用异常控制正常的程序流程
  2. catch 顺序——具体异常在前,通用异常在后
  3. finally 总会执行——即使 try 中有 return
  4. throw vs throw ex——throw 保留原始堆栈,throw ex 重置堆栈
  5. 异常过滤器性能更好——when 子句匹配失败时不会 unwind 堆栈
  6. 不要捕获致命异常——OutOfMemoryException、StackOverflowException 等无法安全处理
  7. 记录足够信息——异常消息、类型、堆栈、自定义数据等

Released under the MIT License.