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. 不要捕获致命异常——OutOfMemoryExceptionStackOverflowException 等无法安全处理
  7. 记录足够信息——异常消息、类型、堆栈、自定义数据等

Released under the MIT License.