异常处理
什么是异常?
核心概念
异常是程序运行时发生的错误或意外情况。.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
- ✅ 使用 throw
- ❌ 使用 throw ex
try
{
// 某些操作
DoSomething();
}
catch (Exception ex)
{
// 记录日志
Log.Error(ex);
// ✅ 保持原始堆栈跟踪
throw;
}
try
{
// 某些操作
DoSomething();
}
catch (Exception ex)
{
// 记录日志
Log.Error(ex);
// ❌ 丢失原始堆栈跟踪
throw ex;
}
区别
throw;
保留完整的堆栈跟踪throw ex;
会重置堆栈跟踪,丢失原始错误位置
抛出自定义异常
// 定义自定义异常
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
- ❌ 手动释放
// 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()
}
// 手动管理资源(不推荐)
StreamReader reader = null;
try
{
reader = new StreamReader(path);
var content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
}
finally
{
// 必须记得释放资源
reader?.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)
{
// 处理异常
}
性能考虑
性能影响
异常处理有性能开销,应该:
- 不要用异常控制正常流程 - 异常应该用于异常情况
- 避免频繁抛出异常 - 考虑使用返回值或 TryParse 模式
- 异常是昂贵的 - 捕获和抛出异常需要创建堆栈跟踪
// ✅ 好:使用 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
语句管理资源 - 不要用异常控制程序流程
- 异常消息要清晰有意义