一、为什么说C#默认异常处理不够用?

咱们先来看个简单的例子。假设你正在开发一个电商系统,用户下单时需要扣减库存:

// 技术栈:C# .NET 6
public class InventoryService
{
    public void DeductStock(int productId, int quantity)
    {
        // 这里直接操作数据库扣减库存
        using var connection = new SqlConnection("连接字符串");
        connection.Execute(
            "UPDATE Products SET Stock = Stock - @quantity WHERE Id = @productId",
            new { productId, quantity });
    }
}

这个代码看起来没什么问题,但如果数据库连接失败、库存不足或者商品不存在时,程序就会直接抛出异常。默认情况下,这些异常会沿着调用栈向上冒泡,最终可能导致整个程序崩溃。

更糟糕的是,很多开发者会这样处理异常:

try {
    // 业务代码
} catch {
    // 什么都不做
}

这种"吃掉异常"的做法就像把垃圾扫到地毯下面,表面上程序还在运行,实际上问题越积越多。

二、异常处理的三个层次

1. 基础防护层

首先我们要确保异常能被正确捕获。改进刚才的例子:

public class InventoryService
{
    public OperationResult DeductStock(int productId, int quantity)
    {
        try {
            using var connection = new SqlConnection("连接字符串");
            var affectedRows = connection.Execute(
                "UPDATE Products SET Stock = Stock - @quantity WHERE Id = @productId AND Stock >= @quantity",
                new { productId, quantity });
                
            return affectedRows > 0 
                ? OperationResult.Success()
                : OperationResult.Fail("库存不足或商品不存在");
        }
        catch (SqlException ex) {
            // 记录详细的错误日志
            Logger.Error(ex, "扣减库存时数据库错误");
            return OperationResult.Fail("系统繁忙,请稍后再试");
        }
        catch (Exception ex) {
            Logger.Error(ex, "扣减库存时未知错误");
            return OperationResult.Fail("系统异常");
        }
    }
}

// 通用的操作结果封装
public class OperationResult
{
    public bool Success { get; set; }
    public string Message { get; set; }
    
    public static OperationResult Success() => new() { Success = true };
    public static OperationResult Fail(string msg) => new() { Success = false, Message = msg };
}

2. 业务恢复层

有些错误是可以恢复的。比如网络波动导致的失败,可以自动重试:

public OperationResult DeductStockWithRetry(int productId, int quantity)
{
    const int maxRetry = 3;
    for (int i = 0; i < maxRetry; i++) {
        var result = DeductStock(productId, quantity);
        if (result.Success) return result;
        
        if (i < maxRetry - 1) {
            Thread.Sleep(1000 * (i + 1)); // 指数退避
        }
    }
    return OperationResult.Fail("操作失败,请稍后再试");
}

3. 全局兜底层

最后,我们需要在应用顶层设置全局异常处理:

// 在ASP.NET Core中
public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext context)
    {
        try {
            await _next(context);
        }
        catch (Exception ex) {
            // 记录完整错误信息
            Logger.Error(ex, "全局异常捕获");
            
            // 返回友好的错误响应
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new {
                success = false,
                message = "系统繁忙,请稍后再试"
            });
        }
    }
}

三、进阶异常处理技巧

1. 异常过滤器

在ASP.NET Core中,可以使用异常过滤器更精细地控制:

public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is BusinessException ex) {
            context.Result = new JsonResult(new {
                success = false,
                message = ex.Message
            }) { StatusCode = 400 };
            context.ExceptionHandled = true;
        }
    }
}

// 注册过滤器
builder.Services.AddControllers(options => {
    options.Filters.Add<CustomExceptionFilter>();
});

2. Polly弹性策略

对于可能暂时性失败的场景,可以使用Polly库:

// 安装NuGet包:Polly
var retryPolicy = Policy
    .Handle<SqlException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(3, retryAttempt => 
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

await retryPolicy.ExecuteAsync(async () => {
    await DeductStockAsync(productId, quantity);
});

四、异常处理最佳实践

  1. 记录完整上下文:不仅要记录错误消息,还要记录当时的业务数据
  2. 区分错误类型:业务异常要给用户友好提示,系统异常要记录详细日志
  3. 避免过度捕获:不要捕获你不打算处理的异常
  4. 资源清理:使用using或finally确保资源释放
  5. 性能考虑:异常处理是有开销的,不要用异常来控制正常流程
// 不好的做法:用异常控制流程
try {
    var product = GetProduct(9999);
    // 处理商品
} catch (NotFoundException) {
    // 商品不存在的处理
}

// 好的做法:明确的状态检查
var product = GetProduct(9999);
if (product == null) {
    // 商品不存在的处理
} else {
    // 处理商品
}

五、总结

C#的默认异常处理机制给了我们基础保障,但在生产环境中远远不够。通过建立多层次的异常处理体系,结合适当的工具和策略,我们可以构建出真正健壮的应用系统。记住,好的异常处理不是要防止所有错误,而是要确保错误发生时系统能优雅降级,并给用户合理的反馈。