C# 异常处理

程序运行总会遇到意外——文件不存在、网络中断、参数非法、内存不够。异常处理就是优雅地应对这些意外,让程序不崩溃、给出有意义的错误信息。C# 的异常机制设计清晰、表达力强,这一篇我们完整过一遍。

一、异常是什么?

异常(Exception)是程序运行时发生的不正常情况。C# 所有异常都继承自 System.Exception。当异常发生:

异常 vs 返回错误码:异常更适合不可预期的错误(IO 失败、网络中断),返回值更适合预期的业务结果(用户没找到、密码错误)。

二、try / catch / finally 基本结构

try
{
    int[] nums = { 1, 2, 3 };
    Console.WriteLine(nums[10]);   // 越界
}
catch (IndexOutOfRangeException ex)
{
    // 捕获特定异常类型
    Console.WriteLine($"索引错误: {ex.Message}");
}
catch (Exception ex)
{
    // 捕获所有异常(放在最后)
    Console.WriteLine($"未知错误: {ex.Message}");
}
finally
{
    // 不管是否异常都执行(资源清理)
    Console.WriteLine("清理完成");
}

// 异常传播:没匹配 catch 会向外抛
// finally 始终执行,即使 try/catch 里 return

规则:

三、throw 抛出异常

public decimal Divide(int a, int b)
{
    if (b == 0)
        throw new DivideByZeroException("除数不能为零");

    return (decimal)a / b;
}

// 调用方
try
{
    decimal r = Divide(10, 0);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine(ex.Message);   // "除数不能为零"
}

// 重新抛出保留堆栈
try { /* ... */ }
catch (Exception ex)
{
    Log(ex);
    throw;       // ✅ 保留原堆栈(不要写 throw ex;)
}

// 抛出新异常并包装原因
catch (Exception ex) when (ex is IOException or SQLException)
{
    throw new MyAppException("数据处理失败", ex);   // inner exception
}

几个关键实践:

四、常见 .NET 异常类型

// 常见系统异常(按需捕获)
Exception                    // 所有异常的基类
SystemException              // 系统级异常基类
├── NullReferenceException       // 访问 null 引用
├── IndexOutOfRangeException     // 数组越界
├── ArgumentException            // 参数无效(基类)
│   └── ArgumentNullException    // 参数为 null
│   └── ArgumentOutOfRangeException
├── InvalidOperationException
├── InvalidCastException
├── FormatException              // 字符串格式错误(Parse 失败)
├── OverflowException            // 算术溢出
├── DivideByZeroException        // 除零
├── NotImplementedException
├── TimeoutException
├── StackOverflowException       // 栈溢出(不可捕获)
├── OutOfMemoryException         // 内存不足(通常不可恢复)
├── IOException                  // IO 异常基类
│   ├── FileNotFoundException
│   └── DirectoryNotFoundException
└── ApplicationException(已不推荐使用)

日常捕获的常见异常:

五、自定义异常

业务异常建议自定义,让代码更清晰:

// 自定义异常:继承 Exception,以 Exception 结尾命名
public class UserNotFoundException : Exception
{
    public string UserId { get; }

    public UserNotFoundException(string userId)
        : base($"用户 {userId} 不存在")
    {
        UserId = userId;
    }

    public UserNotFoundException(string userId, Exception inner)
        : base($"用户 {userId} 不存在", inner)
    {
        UserId = userId;
    }
}

// 抛出
public User GetUser(string id)
{
    if (id == "deleted")
        throw new UserNotFoundException(id);
    return new User { Id = id };
}

// 捕获
try
{
    GetUser("deleted");
}
catch (UserNotFoundException ex)
{
    Console.WriteLine($"{ex.UserId} - {ex.Message}");
}

自定义异常的规则:

六、when 异常过滤器(C# 6+)

catch 加 when 条件,更精细控制:

// when 异常过滤器(C# 6+):catch 加条件
try
{
    Dangerous();
}
catch (HttpRequestException ex) when (ex.StatusCode == 404)
{
    Console.WriteLine("资源不存在");
}
catch (HttpRequestException ex) when (ex.StatusCode >= 500)
{
    Console.WriteLine("服务器错误");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"其他 HTTP 错误: {ex.Message}");
}

// 过滤器内可以打日志(不进入 catch 的话堆栈保留)
catch (Exception ex) when (Log(ex)) { }
// Log 返回 false 时不进入,继续向外抛

when 的优势:

七、using 与 IDisposable(资源释放)

异常处理的一个重要场景是资源释放——文件、数据库连接、网络流。using 语句确保即使抛异常也能 Dispose:

// using 语句:自动 Dispose(即使抛异常)
using (var fs = new FileStream("a.txt", FileMode.Open))
{
    // 用 fs
}   // 离开花括号自动调 fs.Dispose()

// using 声明(C# 8+,无需花括号)
using var fs2 = new FileStream("a.txt", FileMode.Open);
// 在方法结束时自动 Dispose

// 实现 IDisposable 的类必须用 using 或手动 Dispose
// 如:FileStream、HttpClient、SqlConnection、Bitmap

public class Resource : IDisposable
{
    public void Dispose()
    {
        // 释放资源
        GC.SuppressFinalize(this);
    }
}

规则:

八、异常处理最佳实践

九、TryXxx 模式 vs 异常

// TryXxx 模式:返回 bool,不抛异常,适合"预期的失败"
if (int.TryParse("abc", out int n))
    Console.WriteLine($"成功: {n}");
else
    Console.WriteLine("不是数字");

// Parse 模式:抛异常,适合"不该失败"的场景
int n2 = int.Parse("123");   // 失败抛 FormatException

// 建议:用户输入用 TryParse,内部数据用 Parse
// Parse 性能更高(TryParse 在成功时一样快,失败时更快)

.NET 里大量 API 提供 TryXxx 版本:int.TryParseDictionary.TryGetValueQueue.TryDequeue

十、异步异常

// async 方法抛异常,会被包进 Task 里
async Task<int> GetAsync()
{
    throw new InvalidOperationException("出错");
}

// 调用:await 时 unwrap 异常
try
{
    await GetAsync();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}

// Task.WhenAny / WhenAll 异常
var t1 = Task.Run(() => throw new Exception("A"));
var t2 = Task.Run(() => throw new Exception("B"));
try
{
    await Task.WhenAll(t1, t2);
}
catch (Exception ex) when (ex.Message == "A")
{
    // 只捕获第一个抛出的异常
}
// 想看所有异常:从 Task 异常属性读

十一、全局异常处理

有些异常总会逃出 try/catch。需要全局兜底:

// 1. 主线程未捕获异常
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
    var ex = (Exception)e.ExceptionObject;
    LogError(ex);
};

// 2. Windows Forms / WPF
Application.ThreadException += (s, e) => LogError(e.Exception);

// 3. ASP.NET Core 全局中间件
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var ex = context.Features.Get<IExceptionHandlerPathFeature>()?.Error;
        await LogAsync(ex);
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("Internal Error");
    });
});

// 4. TaskScheduler 未观察异常
TaskScheduler.UnobservedTaskException += (s, e) =>
{
    LogError(e.Exception);
    e.SetObserved();   // 标记已处理
};

十二、性能注意

小结

异常处理是工程化的必备技能。记住几条:具体捕获不要吞throw 保留堆栈using 释放资源TryXxx 用于预期失败。下一篇我们看文件 IO——异常处理的最佳实战场景。

← 上一篇 C# LINQ

下一篇 C# 文件 IO

✈️💬