一、为什么你的C#代码总是莫名其妙报错
写C#代码最让人头疼的,莫过于运行时报出一堆看不懂的异常。比如常见的NullReferenceException(空引用)、IndexOutOfRangeException(数组越界),或者是更让人崩溃的AggregateException(聚合异常)。这些错误往往不是因为你逻辑写错了,而是.NET默认的异常处理机制在"帮倒忙"。
举个典型例子:
// 技术栈:.NET 6 Console Application
try
{
var list = new List<string> { "a", "b" };
Console.WriteLine(list[2]); // 这里会抛出IndexOutOfRangeException
}
catch (Exception ex)
{
// 默认只打印异常类型和消息
Console.WriteLine($"出错了: {ex.Message}");
// 实际开发中我们需要更多上下文信息!
}
这个简单的例子暴露了默认异常处理的三大痛点:
- 错误信息过于简略(只有"Index was outside the bounds of the array")
- 没有记录错误发生时的变量状态
- 缺乏完整的调用堆栈上下文
二、解剖.NET异常处理的"黑匣子"
2.1 异常传播机制
在.NET中,异常会沿着调用堆栈向上冒泡。如果没有任何catch块处理,最终会被CLR的默认处理器捕获,然后程序崩溃。这个过程就像传话游戏,中间任何环节没处理好,原始信息就会失真。
2.2 异常类型体系
所有异常都继承自System.Exception,主要分为两类:
- SystemException(系统级异常,如OutOfMemoryException)
- ApplicationException(应用级异常,建议自定义异常继承此类)
看个自定义异常的例子:
// 技术栈:.NET 6 Class Library
public class PaymentFailedException : ApplicationException
{
public decimal Amount { get; }
public string PaymentMethod { get; }
// 包含业务上下文的自定义异常
public PaymentFailedException(decimal amount, string method, string message)
: base(message)
{
Amount = amount;
PaymentMethod = method;
}
// 重写ToString提供更详细的信息
public override string ToString() =>
$"支付异常: {Message}\n金额: {Amount}\n支付方式: {PaymentMethod}\n{StackTrace}";
}
2.3 异常吞噬陷阱
最常见的反模式就是盲目捕获所有异常却不做处理:
try
{
DangerousOperation();
}
catch (Exception)
{
// 空的catch块是万恶之源!
}
三、异常处理进阶实战技巧
3.1 智能日志记录
使用Serilog+NLog组合拳:
// 技术栈:.NET 6 + Serilog
try
{
var user = GetUser(userId);
_logger.Information("获取用户成功 {@User}", user); // 结构化日志
}
catch (UserNotFoundException ex)
{
_logger.Warning(ex, "用户不存在,ID: {UserId}", userId);
throw new FriendlyException("您查询的用户不存在"); // 转换为用户友好异常
}
catch (DbException ex) when (ex.IsTransient) // 条件捕获
{
_logger.Error(ex, "数据库暂时性错误,准备重试");
await Task.Delay(1000);
RetryOperation();
}
3.2 全局异常拦截
ASP.NET Core中的全局过滤器:
// 技术栈:.NET 6 Web API
public class GlobalExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var ex = context.Exception;
var error = new {
Code = ex switch {
ValidationException => 400,
_ => 500
},
Message = ex.Message,
Detail = ex.GetType().Name,
StackTrace = Debugger.IsAttached ? ex.StackTrace : null
};
context.Result = new JsonResult(error) {
StatusCode = error.Code
};
context.ExceptionHandled = true;
}
}
3.3 异步异常处理要点
Task异常处理的特殊之处:
// 技术栈:.NET 6 Async/Await
async Task ProcessBatchAsync()
{
var tasks = new List<Task>();
try
{
tasks.Add(ProcessItemAsync(1));
tasks.Add(ProcessItemAsync(2));
await Task.WhenAll(tasks); // 这里会聚合所有异常
}
catch (AggregateException ae) // 异步场景特有
{
foreach (var ex in ae.Flatten().InnerExceptions)
{
_logger.Error("批量处理出错: {Msg}", ex.Message);
}
}
}
四、异常处理最佳实践清单
分级处理原则:
- 系统级异常(如内存不足):立即终止
- 业务异常(如订单不存在):转换后抛出
- 预期内异常(如网络超时):自动重试
上下文丰富化:
try { // 业务代码 } catch (Exception ex) { // 添加上下文数据 ex.Data["Timestamp"] = DateTime.UtcNow; ex.Data["UserId"] = currentUserId; throw; // 重新抛出保留堆栈 }防御性编程技巧:
- 空值检查:使用?.和??运算符
- 集合操作:Always check Count/Length before access
- 类型转换:优先使用as而不是强制转换
性能考量:
- 异常构造开销大(约1000次/ms)
- 高频路径避免异常(如用TryParse代替Parse)
- 预检查优于事后捕获
五、从框架层面改进异常处理
5.1 错误码模式
定义明确的错误码体系:
public enum ErrorCode
{
Success = 0,
InvalidInput = 1001,
DbTimeout = 2001,
// ...
}
public class ApiResult<T>
{
public ErrorCode Code { get; set; }
public string Message { get; set; }
public T Data { get; set; }
public static ApiResult<T> Fail(ErrorCode code, string msg) =>
new() { Code = code, Message = msg };
}
5.2 AOP切面处理
使用Castle DynamicProxy实现:
public class ExceptionInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
try
{
invocation.Proceed();
}
catch (Exception ex)
{
var method = invocation.Method;
_logger.Error(ex, "方法执行失败: {Class}.{Method}",
method.DeclaringType?.Name, method.Name);
throw new ServiceException("服务调用失败", ex);
}
}
}
六、总结与展望
现代C#开发中,异常处理已经不再是简单的try-catch游戏。我们需要建立分层的防御体系:
- 最外层:全局异常过滤器(记录+转换)
- 中间层:领域异常(携带业务语义)
- 最内层:防御性验证(避免异常发生)
未来趋势:
- 更智能的异常分析工具(如Application Insights的失败关联)
- 基于Source Generator的编译时异常检查
- 与Nullable Reference Types的深度集成
记住:好的异常处理系统应该像飞机的黑匣子,不仅能记录问题,还能帮助快速定位和复现问题。你的异常处理策略,决定了系统在极端情况下的表现,这往往是区分普通应用和企业级应用的关键标志。
评论