一、为什么说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);
});
四、异常处理最佳实践
- 记录完整上下文:不仅要记录错误消息,还要记录当时的业务数据
- 区分错误类型:业务异常要给用户友好提示,系统异常要记录详细日志
- 避免过度捕获:不要捕获你不打算处理的异常
- 资源清理:使用using或finally确保资源释放
- 性能考虑:异常处理是有开销的,不要用异常来控制正常流程
// 不好的做法:用异常控制流程
try {
var product = GetProduct(9999);
// 处理商品
} catch (NotFoundException) {
// 商品不存在的处理
}
// 好的做法:明确的状态检查
var product = GetProduct(9999);
if (product == null) {
// 商品不存在的处理
} else {
// 处理商品
}
五、总结
C#的默认异常处理机制给了我们基础保障,但在生产环境中远远不够。通过建立多层次的异常处理体系,结合适当的工具和策略,我们可以构建出真正健壮的应用系统。记住,好的异常处理不是要防止所有错误,而是要确保错误发生时系统能优雅降级,并给用户合理的反馈。
评论