一、为什么你的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}"); 
    // 实际开发中我们需要更多上下文信息!
}

这个简单的例子暴露了默认异常处理的三大痛点:

  1. 错误信息过于简略(只有"Index was outside the bounds of the array")
  2. 没有记录错误发生时的变量状态
  3. 缺乏完整的调用堆栈上下文

二、解剖.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);
        }
    }
}

四、异常处理最佳实践清单

  1. 分级处理原则

    • 系统级异常(如内存不足):立即终止
    • 业务异常(如订单不存在):转换后抛出
    • 预期内异常(如网络超时):自动重试
  2. 上下文丰富化

    try
    {
        // 业务代码
    }
    catch (Exception ex)
    {
        // 添加上下文数据
        ex.Data["Timestamp"] = DateTime.UtcNow;
        ex.Data["UserId"] = currentUserId;
        throw; // 重新抛出保留堆栈
    }
    
  3. 防御性编程技巧

    • 空值检查:使用?.和??运算符
    • 集合操作:Always check Count/Length before access
    • 类型转换:优先使用as而不是强制转换
  4. 性能考量

    • 异常构造开销大(约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游戏。我们需要建立分层的防御体系:

  1. 最外层:全局异常过滤器(记录+转换)
  2. 中间层:领域异常(携带业务语义)
  3. 最内层:防御性验证(避免异常发生)

未来趋势:

  • 更智能的异常分析工具(如Application Insights的失败关联)
  • 基于Source Generator的编译时异常检查
  • 与Nullable Reference Types的深度集成

记住:好的异常处理系统应该像飞机的黑匣子,不仅能记录问题,还能帮助快速定位和复现问题。你的异常处理策略,决定了系统在极端情况下的表现,这往往是区分普通应用和企业级应用的关键标志。