123 lines
5.4 KiB
Markdown
123 lines
5.4 KiB
Markdown
# 2.2 异常处理
|
||
|
||
---
|
||
|
||
## 【强制】
|
||
|
||
1. **业务异常处理**:
|
||
* **优先校验**:应优先使用条件判断(`if`)来检查业务规则是否满足,在发现问题时提前拦截。
|
||
* **抛出异常**:当校验不通过需中断流程时,应抛出 `BusinessException` 或其子类,而不是 `System.Exception`。
|
||
* **消息友好**:异常消息(Message)将直接作为错误提示返回给前端/客户端,必须确保消息内容用户友好且无敏感技术信息。
|
||
* **记录异常**:在业务逻辑中抛出异常时,必须记录异常信息和堆栈到日志中,以便后续分析和排查问题。
|
||
* *正例*:
|
||
```csharp
|
||
if (order.Status != OrderStatus.Created)
|
||
{
|
||
throw new BusinessException("当前订单状态不允许取消");
|
||
}
|
||
```
|
||
|
||
2. **全局异常处理**:
|
||
* **中间件接管**:严禁在 Controller 层进行通用的 `try-catch` 捕获。Aegis 框架已内置全局异常处理中间件,会自动捕获未处理异常并转换为标准 JSON 响应。
|
||
* **响应格式**:框架会将未处理异常统一转换为如下格式:
|
||
```json
|
||
{
|
||
"code": 500,
|
||
"message": "服务器内部错误 / 业务错误提示",
|
||
"data": null,
|
||
"success": false
|
||
}
|
||
```
|
||
|
||
3. **必须通过预检查规避可预见的异常**:对于 `NullReferenceException`, `ArgumentNullException`, `IndexOutOfRangeException` 等可以通过代码检查来避免的运行时异常,不应该使用 `try-catch` 来处理。
|
||
|
||
|
||
*说明*:异常处理的性能开销远高于条件判断。编写健壮的代码,通过防御性编程(如卫语句)来处理可预见的问题,是程序员的基本素养。
|
||
|
||
*反例*:
|
||
```csharp
|
||
try
|
||
{
|
||
// obj 可能为 null
|
||
obj.DoSomething();
|
||
}
|
||
catch (NullReferenceException ex)
|
||
{
|
||
_logger.LogError(ex, "obj was null");
|
||
}
|
||
```
|
||
|
||
*正例*:
|
||
```csharp
|
||
if (obj is null)
|
||
{
|
||
throw new ArgumentNullException(nameof(obj));
|
||
}
|
||
obj.DoSomething();
|
||
```
|
||
|
||
2. **禁止使用异常作为流程控制**:异常是为程序运行中的意外情况设计的,不应用于正常的业务逻辑判断和流程跳转。
|
||
|
||
3. **精确捕获异常类型**:`catch` 时应尽可能捕获具体的异常类型,而不是宽泛的 `System.Exception`。对不同的异常类型应有不同的处理逻辑。
|
||
|
||
*说明*:对大段代码进行笼统的 `try-catch`,会使程序无法对特定错误做出正确响应,不利于问题定位和系统稳定性。
|
||
|
||
*正例*:
|
||
```csharp
|
||
try
|
||
{
|
||
// ... 数据库操作
|
||
}
|
||
catch (SqlException ex)
|
||
{
|
||
// 处理数据库相关的特定错误
|
||
_logger.LogError(ex, "A database error occurred.");
|
||
}
|
||
catch (TimeoutException ex)
|
||
{
|
||
// 处理连接超时错误
|
||
_logger.LogWarning(ex, "The database connection timed out.");
|
||
}
|
||
```
|
||
|
||
4. **捕获后必须处理或转抛**:捕获异常后,必须进行处理(如记录日志、事务回滚、向用户返回错误信息等)。如果当前层无法处理,应使用 `throw;` 语句将原始异常向上抛出。严禁“吞掉”异常(即空的 `catch` 块)。
|
||
|
||
5. **必须使用 `using` 语句管理资源**:对于实现了 `IDisposable` 接口的资源对象(如流、数据库连接、HttpClient 等),必须使用 `using` 语句来确保资源的正确释放。
|
||
|
||
*说明*:`using` 语句会自动生成 `try-finally` 块,并调用 `Dispose()` 方法,这是 .NET 中管理非托管资源的最安全、最简洁的方式。
|
||
|
||
*正例*:
|
||
```csharp
|
||
using (var stream = new FileStream("path/to/file", FileMode.Open))
|
||
{
|
||
// ... 对 stream 的操作
|
||
}
|
||
// 此处 stream 已被自动关闭和释放
|
||
```
|
||
|
||
6. **禁止在 `finally` 块中使用 `return`**:`finally` 块中的 `return` 会覆盖 `try` 或 `catch` 块中的 `return` 或异常,导致非预期的行为。
|
||
|
||
## 【推荐】
|
||
|
||
1. **启用并遵循可空引用类型 (`Nullable Reference Types`)**:在项目(`.csproj`)中启用 `<Nullable>enable</Nullable>`,让编译器帮助你从静态层面发现潜在的 `NullReferenceException`。
|
||
|
||
2. **优雅地处理空引用**:
|
||
- **级联调用**:使用空条件运算符 `?.` 和 `?[]` 来安全地访问可能为 null 的对象的成员或索引。
|
||
*正例*: `string? postalCode = customer?.Address?.PostalCode;`
|
||
- **返回值**:方法的返回值可以为 null,但必须在方法签名和 XML 注释中明确指出。调用方有责任处理 null 情况。
|
||
|
||
3. **使用自定义异常表达业务错误**:应定义有具体业务含义的自定义异常(继承自 `System.Exception`),而不是直接抛出泛泛的 `new Exception()`。
|
||
|
||
*正例*:
|
||
```csharp
|
||
public class OrderNotFoundException : Exception
|
||
{
|
||
public OrderNotFoundException(int orderId)
|
||
: base($"Order with ID {orderId} was not found.") { }
|
||
}
|
||
```
|
||
|
||
## 【参考】
|
||
|
||
1. **API 的错误响应**:对于对外暴露的 API(如 ASP.NET Core Web API),应建立一个全局异常处理中间件。该中间件负责捕获所有未处理的异常,记录日志,并将其转换为统一、标准的 JSON 错误响应格式返回给客户端,而不是直接暴露异常堆栈信息。
|