Skip to main content

异常处理

什么是异常?

核心概念

异常是程序运行时发生的错误或意外情况。.NET 提供了结构化的异常处理机制,通过 try-catch-finally 语句处理异常。

异常基础

Try-Catch-Finally

try
{
// 可能抛出异常的代码
int result = 10 / 0; // DivideByZeroException
}
catch (DivideByZeroException ex)
{
// 处理特定异常
Console.WriteLine($"除零错误: {ex.Message}");
}
catch (Exception ex)
{
// 处理其他异常
Console.WriteLine($"错误: {ex.Message}");
}
finally
{
// 无论是否发生异常都会执行
Console.WriteLine("清理资源");
}

异常层次结构

graph TD
A[Object] --> B[Exception]
B --> C[SystemException]
B --> D[ApplicationException]
C --> E[NullReferenceException]
C --> F[ArgumentException]
C --> G[InvalidOperationException]
C --> H[IOException]
F --> I[ArgumentNullException]
F --> J[ArgumentOutOfRangeException]

常见异常类型

异常类型说明示例
NullReferenceException访问 null 对象的成员string s = null; s.Length
ArgumentNullException参数为 null方法参数检查
ArgumentException参数无效参数值不符合要求
InvalidOperationException操作无效在错误的状态下调用方法
IndexOutOfRangeException数组索引越界arr[10] 但数组长度为 5
KeyNotFoundException字典键不存在dict["key"]
FileNotFoundException文件不存在打开不存在的文件
UnauthorizedAccessException无权限访问访问受保护的资源
TimeoutException操作超时网络请求超时

抛出异常

throw vs throw ex

try
{
// 某些操作
DoSomething();
}
catch (Exception ex)
{
// 记录日志
Log.Error(ex);

// ✅ 保持原始堆栈跟踪
throw;
}

抛出自定义异常

// 定义自定义异常
public class OrderNotFoundException : Exception
{
public string OrderId { get; }

public OrderNotFoundException(string orderId)
: base($"订单不存在: {orderId}")
{
OrderId = orderId;
}

public OrderNotFoundException(string orderId, Exception innerException)
: base($"订单不存在: {orderId}", innerException)
{
OrderId = orderId;
}
}

// 使用自定义异常
public Order GetOrder(string orderId)
{
var order = _repository.FindById(orderId);
if (order == null)
{
throw new OrderNotFoundException(orderId);
}
return order;
}

异常过滤器 (Exception Filters)

try
{
// 某些操作
ProcessData();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// 只处理 404 错误
Console.WriteLine("资源不存在");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
// 只处理 401 错误
Console.WriteLine("未授权");
}
catch (HttpRequestException ex)
{
// 处理其他 HTTP 错误
Console.WriteLine($"HTTP 错误: {ex.StatusCode}");
}

// 更复杂的条件
catch (Exception ex) when (LogException(ex))
{
// 永远不会执行,因为 LogException 返回 false
// 但可以用来记录日志而不捕获异常
}

bool LogException(Exception ex)
{
Log.Error(ex);
return false; // 不捕获,继续向上传播
}

实战案例

案例1:参数验证

public class UserService
{
public User CreateUser(string name, string email, int age)
{
// 参数验证
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name), "用户名不能为空");
}

if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentNullException(nameof(email), "邮箱不能为空");
}

if (age < 0 || age > 150)
{
throw new ArgumentOutOfRangeException(nameof(age), age, "年龄必须在 0-150 之间");
}

// 创建用户
return new User { Name = name, Email = email, Age = age };
}
}

案例2:资源管理(using 语句)

// using 语句自动处理资源释放
public void ReadFile(string path)
{
using (var reader = new StreamReader(path))
{
var content = reader.ReadToEnd();
Console.WriteLine(content);
}
// reader 自动调用 Dispose()
}

// C# 8.0+ using 声明
public void ReadFile(string path)
{
using var reader = new StreamReader(path);
var content = reader.ReadToEnd();
Console.WriteLine(content);
// 在方法结束时自动调用 Dispose()
}

案例3:重试机制

public async Task<T> RetryAsync<T>(
Func<Task<T>> operation,
int maxRetries = 3,
TimeSpan? delay = null)
{
delay ??= TimeSpan.FromSeconds(1);

for (int i = 0; i < maxRetries; i++)
{
try
{
return await operation();
}
catch (Exception ex) when (i < maxRetries - 1)
{
_logger.LogWarning($"操作失败,{delay.Value.TotalSeconds}秒后重试 ({i + 1}/{maxRetries})");
await Task.Delay(delay.Value);
}
}

// 最后一次尝试,不捕获异常
return await operation();
}

// 使用
var data = await RetryAsync(
async () => await _httpClient.GetStringAsync("https://api.example.com/data"),
maxRetries: 3,
delay: TimeSpan.FromSeconds(2)
);

案例4:统一异常处理(ASP.NET Core)

// 全局异常处理中间件
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;

public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "发生未处理的异常");

context.Response.ContentType = "application/json";

var (statusCode, message) = exception switch
{
ArgumentNullException => (400, "请求参数不能为空"),
ArgumentException => (400, exception.Message),
UnauthorizedAccessException => (401, "未授权"),
KeyNotFoundException => (404, "资源不存在"),
InvalidOperationException => (400, exception.Message),
TimeoutException => (408, "请求超时"),
_ => (500, "服务器内部错误")
};

context.Response.StatusCode = statusCode;

var response = new
{
error = new
{
message,
statusCode,
timestamp = DateTime.UtcNow
}
};

await context.Response.WriteAsJsonAsync(response);
}
}

// 注册中间件
app.UseMiddleware<ExceptionHandlingMiddleware>();

案例5:业务异常 vs 系统异常

// 业务异常
public class BusinessException : Exception
{
public string ErrorCode { get; }

public BusinessException(string errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}
}

// 具体的业务异常
public class InsufficientBalanceException : BusinessException
{
public decimal RequiredAmount { get; }
public decimal CurrentBalance { get; }

public InsufficientBalanceException(decimal requiredAmount, decimal currentBalance)
: base("INSUFFICIENT_BALANCE", $"余额不足:需要 {requiredAmount},当前 {currentBalance}")
{
RequiredAmount = requiredAmount;
CurrentBalance = currentBalance;
}
}

// 使用
public class PaymentService
{
public async Task ProcessPaymentAsync(string userId, decimal amount)
{
var balance = await GetUserBalanceAsync(userId);

if (balance < amount)
{
throw new InsufficientBalanceException(amount, balance);
}

// 处理支付
await DeductBalanceAsync(userId, amount);
}
}

// 统一处理
try
{
await paymentService.ProcessPaymentAsync(userId, 100);
}
catch (BusinessException ex)
{
// 业务异常:返回给用户
return BadRequest(new { error = ex.Message, code = ex.ErrorCode });
}
catch (Exception ex)
{
// 系统异常:记录日志,返回通用错误
_logger.LogError(ex, "支付失败");
return StatusCode(500, "系统错误,请稍后重试");
}

异常处理最佳实践

最佳实践

1. 只捕获你能处理的异常

// ✅ 好:只捕获预期的异常
try
{
var config = File.ReadAllText("config.json");
}
catch (FileNotFoundException)
{
// 使用默认配置
var config = GetDefaultConfig();
}

// ❌ 差:捕获所有异常
try
{
var config = File.ReadAllText("config.json");
}
catch (Exception)
{
// 这可能隐藏了其他问题
}

2. 不要吞掉异常

// ❌ 差:吞掉异常
try
{
DoSomething();
}
catch
{
// 什么都不做,异常被吞掉了
}

// ✅ 好:至少记录日志
try
{
DoSomething();
}
catch (Exception ex)
{
_logger.LogError(ex, "操作失败");
throw;
}

3. 使用具体的异常类型

// ✅ 好:抛出具体的异常
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}

if (age < 0)
{
throw new ArgumentOutOfRangeException(nameof(age), "年龄不能为负数");
}

// ❌ 差:抛出通用异常
if (user == null)
{
throw new Exception("用户为空");
}

4. 异常消息要有意义

// ✅ 好:提供详细信息
throw new InvalidOperationException(
$"无法处理订单 {orderId}:订单状态为 {order.Status},期望状态为 {OrderStatus.Pending}");

// ❌ 差:消息不明确
throw new InvalidOperationException("操作失败");

5. 不要用异常控制流程

// ❌ 差:用异常控制流程
try
{
var user = users[index];
}
catch (IndexOutOfRangeException)
{
user = null;
}

// ✅ 好:用条件判断
var user = index < users.Length ? users[index] : null;

6. 清理资源

// ✅ 好:使用 using 语句
using var connection = new SqlConnection(connectionString);
connection.Open();
// 使用 connection

// ✅ 好:实现 IDisposable
public class ResourceManager : IDisposable
{
private bool _disposed = false;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// 释放托管资源
}

// 释放非托管资源
_disposed = true;
}
}
}

7. 异步方法中的异常

// ✅ 好:异步方法返回 Task
public async Task ProcessAsync()
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "处理失败");
throw;
}
}

// 调用方捕获异常
try
{
await ProcessAsync();
}
catch (Exception ex)
{
// 处理异常
}

性能考虑

性能影响

异常处理有性能开销,应该:

  1. 不要用异常控制正常流程 - 异常应该用于异常情况
  2. 避免频繁抛出异常 - 考虑使用返回值或 TryParse 模式
  3. 异常是昂贵的 - 捕获和抛出异常需要创建堆栈跟踪
// ✅ 好:使用 TryParse 模式
if (int.TryParse(input, out var number))
{
// 使用 number
}
else
{
// 处理无效输入
}

// ❌ 差:使用异常
try
{
var number = int.Parse(input);
}
catch (FormatException)
{
// 处理无效输入
}

总结

关键要点
  • 使用 try-catch-finally 处理异常
  • 只捕获你能处理的异常
  • 使用 throw; 而不是 throw ex; 保留堆栈跟踪
  • 使用具体的异常类型
  • 实现统一的异常处理机制
  • 使用 using 语句管理资源
  • 不要用异常控制程序流程
  • 异常消息要清晰有意义

相关资源