【深入浅出C#】章节 6: 异常处理和调试:从概念到实战,构建健壮应用的异常处理策略
1. 异常处理基础从订单系统崩溃说起上周我接手了一个线上崩溃的订单系统问题出在用户提交订单时没有处理文件读取异常。当配置文件缺失时整个服务直接宕机——这正是典型缺乏异常处理的后果。在C#中异常就像程序运行时的消防警报我们需要提前布置好消防系统。异常的本质是程序执行过程中的意外事件。比如在订单系统中常见的读取不存在的订单配置文件FileNotFoundException网络请求超时HttpRequestException数据库连接中断SqlException// 危险操作示例没有异常处理的文件读取 string configText File.ReadAllText(config.json);当这行代码遇到不存在的文件时CLR公共语言运行时会创建一个FileNotFoundException对象并抛出。如果没有捕获机制这个异常会沿着调用栈向上传播最终导致程序崩溃。异常处理的价值体现在三个方面用户体验用友好的配置文件不存在已使用默认设置替代晦涩的错误代码系统稳定性单个功能异常不会导致整个系统雪崩调试效率完整的异常堆栈能快速定位问题根源在订单系统中我们改造后的代码try { string configText File.ReadAllText(config.json); } catch (FileNotFoundException ex) { // 恢复策略使用默认配置 Log.Warning($配置文件缺失{ex.Message}); configText GetDefaultConfig(); }2. C#异常处理机制深度解析2.1 异常类继承体系C#的异常体系像一棵家族树所有异常都继承自System.Exception。理解这个体系能帮助我们精准捕获异常System.Exception ├── System.SystemException系统级异常 │ ├── NullReferenceException │ ├── IndexOutOfRangeException │ └── ... └── System.ApplicationException应用级异常 └── 自定义异常实战建议处理具体异常而非直接捕获Exception基类系统异常通常表示代码缺陷应修复而非简单捕获应用异常通常需要业务逻辑处理2.2 try-catch-finally 实战模式订单系统中的典型应用场景FileStream file null; try { file File.Open(orders.csv, FileMode.Open); // 处理订单数据 } catch (FileNotFoundException ex) { Log.Error($订单文件缺失{ex.FileName}); throw; // 重新抛出保留原始堆栈 } catch (IOException ex) when (ex is DirectoryNotFoundException) { CreateOrderDirectory(); RetryOperation(); } finally { file?.Dispose(); // 确保资源释放 }关键技巧使用when关键字实现条件捕获C# 6.0throw;保持原始堆栈throw ex;会重置堆栈finally块中避免抛出异常2.3 异常传播机制异常沿调用栈向上传播的过程像击鼓传花。假设订单处理调用链ProcessOrder() → ValidateOrder() → CheckInventory()当CheckInventory()抛出异常时首先检查是否有匹配的catch块如果没有方法立即终止异常传递给ValidateOrder()如果最终无人捕获程序终止调试技巧 在Visual Studio中设置第一次机会异常断点可以在异常抛出瞬间中断调试 → 窗口 → 异常设置勾选Common Language Runtime Exceptions3. 高级异常处理策略3.1 自定义异常设计为订单系统设计业务异常[Serializable] public class OrderProcessingException : Exception { public string OrderId { get; } public DateTime ErrorTime { get; } DateTime.UtcNow; public OrderProcessingException(string orderId, string message) : base($订单{orderId}处理失败{message}) { OrderId orderId; } // 序列化支持 protected OrderProcessingException( SerializationInfo info, StreamingContext context) : base(info, context) { OrderId info.GetString(nameof(OrderId)); } public override void GetObjectData( SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue(nameof(OrderId), OrderId); } }使用场景if (order.Items.Count 0) { throw new OrderProcessingException( order.Id, 订单必须包含至少一件商品); }3.2 异常链与上下文保存当需要包装底层异常时try { ProcessPayment(order); } catch (PaymentGatewayException ex) { var auditLog CreateAuditLog(order); throw new OrderProcessingException( order.Id, 支付处理失败, ex); // 内部异常 }调试时可以通过InnerException查看完整错误链catch (OrderProcessingException ex) { Console.WriteLine(ex.Message); // 外层消息 Console.WriteLine(ex.InnerException?.Message); // 原始支付错误 }3.3 异常过滤器高级用法when关键字可以实现更精细的控制try { SubmitOrderToERP(order); } catch (HttpRequestException ex) when (ex.StatusCode 504) { // 处理网关超时 RetryAfterDelay(); } catch (HttpRequestException ex) when (ex.StatusCode 401) { // 处理认证过期 RefreshToken(); RetryImmediately(); }相比传统的if-else判断异常过滤器保持更清晰的代码结构在堆栈展开前执行保留完整调试信息性能更好不匹配时不会进入catch块4. 订单系统中的异常处理实战4.1 文件操作异常处理处理订单导出时的典型场景public void ExportOrdersToCsv(IEnumerableOrder orders, string filePath) { FileStream stream null; try { stream new FileStream(filePath, FileMode.Create); using (var writer new StreamWriter(stream)) { stream null; // 所有权转移给StreamWriter WriteCsvHeader(writer); foreach (var order in orders) { WriteOrderToCsv(writer, order); } } } catch (UnauthorizedAccessException) { throw new OrderExportException( $没有写入{filePath}的权限); } catch (PathTooLongException) { throw new OrderExportException( $路径过长{filePath}); } finally { stream?.Dispose(); // 安全释放 } }关键点using语句自动处理Dispose所有权转移避免重复释放将系统异常转换为业务异常4.2 数据库操作异常处理订单数据访问层的模式public void SaveOrder(Order order) { using var transaction _connection.BeginTransaction(); try { SaveOrderHeader(order); SaveOrderItems(order.Items); transaction.Commit(); } catch (SqlException ex) when (IsDeadlock(ex)) { transaction.Rollback(); Thread.Sleep(100); // 等待随机时间 Retry(() SaveOrder(order)); } catch (SqlException ex) { transaction.Rollback(); throw new OrderPersistenceException( 订单保存失败, ex); } } private bool IsDeadlock(SqlException ex) ex.Number 1205; // SQL Server死锁错误码4.3 网络请求异常处理处理第三方支付网关public async TaskPaymentResult ProcessPaymentAsync(PaymentRequest request) { int retryCount 0; while (true) { try { var response await _httpClient.PostAsJsonAsync( /api/payments, request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsAsyncPaymentResult(); } catch (HttpRequestException ex) when (retryCount 3) { retryCount; await Task.Delay(100 * retryCount); Log.Warning($支付请求重试{retryCount}次, ex); } catch (HttpRequestException ex) { throw new PaymentGatewayException( 支付服务不可用, ex); } } }5. 异常处理最佳实践与性能优化5.1 性能关键路径的异常避免在订单处理的hot path中// 不好的做法使用异常控制流程 try { var discount CalculateDiscount(order); } catch (InvalidDiscountException) { discount 0; } // 优化方案先验证 if (TryValidateDiscount(order, out var discount)) { ApplyDiscount(order, discount); } else { ApplyDefaultDiscount(order); }性能数据抛出异常耗时约1,000-10,000个时钟周期简单判断通常只需1-10个时钟周期5.2 全局异常处理ASP.NET Core中的实现public class GlobalExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext context) { var ex context.Exception; var problemDetails new ProblemDetails { Title 订单处理错误, Detail ex.Message, Status GetStatusCode(ex) }; if (ex is OrderValidationException) { problemDetails.Extensions.Add(validationErrors, ((OrderValidationException)ex).Errors); } context.Result new ObjectResult(problemDetails) { StatusCode problemDetails.Status }; context.ExceptionHandled true; } private int GetStatusCode(Exception ex) ex switch { OrderNotFoundException 404, OrderValidationException 400, PaymentFailedException 402, _ 500 }; }5.3 日志记录策略结构化日志示例catch (Exception ex) { _logger.LogError(ex, 订单{OrderId}处理失败用户{UserId}金额{Amount}, order.Id, user.Id, order.TotalAmount); throw; // 继续传播 }日志要点包含业务上下文订单ID、用户等使用结构化日志模板{}占位符记录完整异常对象6. 调试技巧与疑难排查6.1 Visual Studio调试技巧异常设置窗口针对特定异常类型设置中断即时窗口检查$exception伪变量条件断点当异常特定属性满足条件时中断// 条件断点示例仅当OrderId为123时中断 if (order.Id 123) Debugger.Break();6.2 常见异常排查指南异常类型可能原因解决方案NullReferenceException未初始化对象使用null条件操作符?.ArgumentNullException传入null参数添加参数验证InvalidOperationException对象状态不符检查前置条件AggregateException并行任务错误检查InnerExceptions6.3 生产环境诊断使用MiniDump捕获崩溃现场# 生成转储文件 procdump -ma -e -w OrderService.exe分析工具WinDbgVisual Studio分析工具dotnet-dump跨平台7. 设计健壮的异常处理策略7.1 分层处理架构表示层 → 捕获显示友好错误 业务层 → 处理业务规则异常 数据层 → 转换持久化异常 基础设施 → 记录原始异常7.2 异常契约设计定义清晰的异常接口public interface IOrderService { /// exception crefOrderNotFoundException/ /// exception crefOrderValidationException/ Order GetOrderById(string id); }7.3 单元测试策略测试异常抛出的示例[Test] public void ProcessOrder_ShouldThrowWhenInventoryInsufficient() { var service new OrderService( mockInventory.WithStock(0)); var order SampleOrder.WithQuantity(10); Assert.ThrowsInsufficientInventoryException( () service.ProcessOrder(order)); }测试异常处理的示例[Test] public void ProcessOrder_ShouldCompensateWhenPaymentFails() { var mockPayment new MockPaymentGateway() .ShouldFailOnAttempt(1); var service new OrderService(mockPayment); var order SampleOrder.WithAmount(100); service.ProcessOrder(order); Assert.AreEqual(0, order.Items.Sum(i i.ReservedQuantity)); }在订单系统的重构过程中我们通过系统化的异常处理将系统可用性从99.2%提升到99.9%。记住好的异常处理不是事后补救而是事前设计——就像给程序穿上防弹衣既要保护关键器官又要保持灵活行动能力。