
1. 为什么需要自定义异常在C#开发中系统提供了丰富的内置异常类型如NullReferenceException、ArgumentException等但在实际业务场景中我们经常需要创建特定领域的异常类型。自定义异常的核心价值在于业务语义明确化当支付失败时抛出PaymentFailedException比通用的Exception更能准确表达问题本质错误分类处理可以针对不同类型的异常实现差异化的处理逻辑携带上下文信息自定义异常可以封装特定领域的错误详情如订单ID、支付金额等我在电商系统开发中就遇到过这样的案例支付模块最初使用标准异常导致错误处理逻辑混乱。后来通过定义PaymentGatewayTimeoutException、InsufficientBalanceException等具体异常使代码可读性和可维护性显著提升。2. 自定义异常的实现规范2.1 基础实现模板一个符合规范的C#自定义异常至少需要以下元素[Serializable] public class MyCustomException : Exception { // 默认构造函数 public MyCustomException() { } // 带消息的构造函数 public MyCustomException(string message) : base(message) { } // 带消息和内层异常的构造函数 public MyCustomException(string message, Exception inner) : base(message, inner) { } // 序列化支持的构造函数 protected MyCustomException( SerializationInfo info, StreamingContext context) : base(info, context) { } }关键点说明必须继承自System.Exception或其子类实现所有标准构造函数特别是序列化构造函数添加[Serializable]特性以支持跨域传输2.2 进阶实现要素在实际项目中我建议补充这些增强设计public class InventoryException : Exception { public string SKU { get; } public int RequestedQuantity { get; } public int AvailableQuantity { get; } public InventoryException(string sku, int requested, int available) : base($Insufficient inventory for {sku}. Requested: {requested}, Available: {available}) { SKU sku; RequestedQuantity requested; AvailableQuantity available; } // 重写ToString()提供详细信息 public override string ToString() ${base.ToString()}, SKU: {SKU}, Requested: {RequestedQuantity}, Available: {AvailableQuantity}; }这种设计可以封装业务特定数据如库存SKU自动生成有意义的错误消息提供详细的诊断信息3. 异常使用的最佳实践3.1 异常命名规范根据我的项目经验异常命名应该后缀必须使用Exception名称应准确描述错误性质如TimeoutException而非MyException1对于模块特定异常可以加模块前缀如PaymentProcessingException反例// 不推荐的命名方式 public class Error1 : Exception {} public class PaymentProblem : Exception {}3.2 异常抛出准则在编写方法时我遵循这些抛出异常的原则精准匹配场景public void ProcessOrder(Order order) { if (order null) throw new ArgumentNullException(nameof(order)); if (order.Items.Count 0) throw new EmptyOrderException(Order must contain items); }提供有意义的错误信息// 不好的做法 throw new Exception(Error occurred); // 好的做法 throw new InvalidOperationException( $Cannot approve order {orderId} because its already in {currentStatus} status);保留原始异常try { // 调用外部服务 } catch (HttpRequestException ex) { throw new PaymentServiceException( Failed to connect to payment gateway, ex); }4. 异常处理的高级技巧4.1 异常过滤器C# 6利用when关键字实现条件捕获try { // 业务代码 } catch (InventoryException ex) when (ex.AvailableQuantity 0) { // 处理部分库存不足的情况 } catch (InventoryException ex) { // 处理完全无库存的情况 }4.2 性能优化建议在性能关键路径中我通常会避免在循环内抛出/捕获异常对于可预见的错误条件如验证失败优先使用返回码模式使用ExceptionDispatchInfo保持调用栈信息ExceptionDispatchInfo.Capture(ex).Throw();4.3 全局异常处理ASP.NET Core中的实现示例app.UseExceptionHandler(appError { appError.Run(async context { var exception context.Features.GetIExceptionHandlerFeature()?.Error; if (exception is MyCustomException customEx) { // 自定义异常的特殊处理 await HandleCustomExceptionAsync(context, customEx); } else { // 通用异常处理 await HandleGenericExceptionAsync(context, exception); } }); });5. 常见问题与解决方案5.1 序列化问题当异常需要跨AppDomain或网络边界传输时必须确保所有自定义属性都标记为[Serializable]实现完整的序列化构造函数重写GetObjectData方法[Serializable] public class CustomException : Exception { public string CustomData { get; } protected CustomException( SerializationInfo info, StreamingContext context) : base(info, context) { CustomData info.GetString(CustomData); } public override void GetObjectData( SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue(CustomData, CustomData); } }5.2 多语言支持对于需要国际化的应用我通常这样设计异常消息public class InternationalizedException : Exception { public string MessageKey { get; } public object[] MessageArgs { get; } public InternationalizedException( string messageKey, params object[] args) : base(ResolveMessage(messageKey, args)) { MessageKey messageKey; MessageArgs args; } private static string ResolveMessage(string key, object[] args) { // 从资源文件加载本地化消息 var template Resources.ResourceManager.GetString(key); return string.Format(template, args); } }5.3 单元测试中的验证使用xUnit测试异常的正确性[Fact] public void Transfer_ShouldThrowInsufficientFundsException() { var account new Account(balance: 100); var ex Assert.ThrowsInsufficientFundsException(() account.Transfer(amount: 200)); Assert.Equal(100, ex.CurrentBalance); Assert.Equal(200, ex.RequestedAmount); }在实际项目中我发现这些异常处理模式特别有用领域特定异常DomainException用于业务规则违反基础设施异常InfrastructureException用于技术层故障使用异常层次结构组织相关异常类型最后分享一个实用技巧在大型项目中我会创建一个Exceptions文件夹按功能模块组织异常类型例如/Exceptions /Payment PaymentException.cs PaymentFailedException.cs PaymentTimeoutException.cs /Inventory InventoryException.cs StockoutException.cs