第二部分:C#进阶
第4章:异常处理与调试
4.2 自定义异常
1. 为什么需要自定义异常?
在C#中,虽然.NET框架提供了丰富的内置异常类型(如ArgumentException、NullReferenceException等),但在实际开发中,我们经常需要根据业务逻辑定义特定的异常类型。自定义异常的优势包括:
- 业务语义清晰:通过异常名称直接表达业务问题(如
InsufficientFundsException) - 携带上下文信息:可添加自定义属性(如订单ID、账户余额等)
- 统一处理逻辑:针对特定异常类型实现集中处理
2. 创建自定义异常的基本步骤
// 示例:银行转账业务的自定义异常
[Serializable]
public class InsufficientFundsException : Exception
{
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
// 基本构造函数
public InsufficientFundsException() { }
// 带消息的构造函数
public InsufficientFundsException(string message)
: base(message) { }
// 带消息和内层异常的构造函数
public InsufficientFundsException(string message, Exception inner)
: base(message, inner) { }
// 带业务数据的构造函数
public InsufficientFundsException(decimal currentBalance, decimal requiredAmount)
: base($"Insufficient funds. Current: {currentBalance}, Required: {requiredAmount}")
{
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
// 序列化支持(可选)
protected InsufficientFundsException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
CurrentBalance = info.GetDecimal(nameof(CurrentBalance));
RequiredAmount = info.GetDecimal(nameof(RequiredAmount));
}
public override void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(CurrentBalance), CurrentBalance);
info.AddValue(nameof(RequiredAmount), RequiredAmount);
}
}
3. 自定义异常的最佳实践
命名规范:
- 类名以"Exception"结尾
- 使用PascalCase命名法
- 名称应明确描述异常场景
继承选择:
- 通常继承自
Exception基类 - 对于参数验证问题可继承
ArgumentException - 对于无效操作可继承
InvalidOperationException
- 通常继承自
序列化支持:
- 添加
[Serializable]特性 - 实现序列化构造函数
- 重写
GetObjectData方法
- 添加
异常信息:
- 提供有意义的错误消息
- 包含相关业务数据
- 避免暴露敏感信息
4. 使用自定义异常示例
public class BankAccount
{
private decimal _balance;
public void Withdraw(decimal amount)
{
if (amount > _balance)
{
throw new InsufficientFundsException(_balance, amount);
}
_balance -= amount;
}
}
// 调用示例
try
{
var account = new BankAccount { Balance = 100 };
account.Withdraw(200);
}
catch (InsufficientFundsException ex)
{
Console.WriteLine($"操作失败:{ex.Message}");
Console.WriteLine($"当前余额:{ex.CurrentBalance}");
Console.WriteLine($"尝试提取:{ex.RequiredAmount}");
// 记录日志或执行补偿逻辑
}
5. 高级主题
异常过滤器(C# 6.0+):
try { ... } catch (InsufficientFundsException ex) when (ex.CurrentBalance > 0) { // 仅当余额大于0时捕获 }异常包装模式:
try { ... } catch (DbUpdateException ex) { throw new DataAccessException("数据库更新失败", ex); }全局异常处理(ASP.NET Core示例):
app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { var exceptionHandler = context.Features.Get<IExceptionHandlerFeature>(); if (exceptionHandler?.Error is BusinessException bizEx) { // 处理业务异常 } }); });
6. 常见问题解答
Q:何时应该创建自定义异常? A:当以下情况时考虑创建:
- 需要表达特定的业务规则违反
- 内置异常无法准确描述问题
- 需要携带额外的上下文信息
Q:自定义异常应该包含多少逻辑? A:异常类应保持简单,主要职责是携带错误信息。避免在异常类中包含复杂的业务逻辑。
Q:如何测试自定义异常? A:使用单元测试验证:
- 异常是否能被正确抛出
- 包含的消息是否正确
- 自定义属性是否被正确设置
- 序列化/反序列化是否正常工作
