LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

C# 异常处理:这样写,Bug少一半!

admin
2026年2月26日 10:26 本文热度 44

try-catch谁都会写,但90%的程序员写的异常处理都有问题!今天分享异常处理的最佳实践,让你的代码更健壮、更容易调试!

一、异常处理的三大误区

误区1:try-catch包一切

// ❌ 错误:把所有代码都包起来
try
{
// 100行代码...
// 出错了根本不知道是哪行
}
catch(Exception ex)
{
// 处理异常
}

误区2:catch了啥也不干

// ❌ 错误:捕获异常却什么都没做
try
{
// 业务代码
}
catch(Exception ex)
{
// 空的!异常被吞了,调试时想哭
}

误区3:用异常控制流程

// ❌ 错误:用异常判断逻辑
try
{
int.Parse(userInput);
}
catch
{
// 输入不是数字
}

// ✅ 正确:用TryParse
if(int.TryParse(userInput,outint result))
{
// 是数字
}
else
{
// 不是数字
}

二、异常处理的核心原则

原则1:只捕获能处理的异常

// ✅ 正确:知道怎么处理才捕获
try
{
var file = File.OpenRead("config.json");
}
catch(FileNotFoundException ex)
{
// 文件不存在,用默认配置
UseDefaultConfig();
}
catch(UnauthorizedAccessException ex)
{
// 没权限,提示用户
ShowError("没有权限读取配置文件");
}
// 其他异常让上层处理

原则2:保留原始异常信息

// ❌ 错误:丢失原始异常
try
{
// 业务代码
}
catch(Exception ex)
{
throw
new BusinessException("业务处理失败");
}

// ✅ 正确:保留原始异常
try
{
// 业务代码
}
catch(Exception ex)
{
throw new BusinessException("业务处理失败", ex);
}

原则3:具体异常优先

// ✅ 正确:先具体后通用
try
{
// 数据库操作
}
catch(SqlException ex)
// 1. 具体的SQL异常
{
LogSqlError(ex);
}
catch(DbException ex)
// 2. 数据库异常
{
LogDbError(ex);
}
catch(Exception ex)
// 3. 其他异常
{
LogError(ex);
throw;
// 处理不了就往上抛
}

三、finally的正确用法

释放资源的好地方

// 传统写法
FileStream fs =null;
try
{
    fs 
= File.OpenRead("data.txt");
// 读取数据
}
finally
{
    fs?.Close();
// 无论如何都会执行
}

// 现代写法(using语句)
using(var fs = File.OpenRead("data.txt"))
{
// 读取数据
}
// 自动释放

// 更现代写法

using var fs = File.OpenRead("data.txt");
// 读取数据

四、自定义异常:什么时候需要?

自定义异常的规则

// 需要自定义异常的场景:
// 1. 有特定的业务含义
// 2. 需要额外的属性
// 3. 需要特殊处理
public class OrderException:Exception
{
public string OrderId {get;}
public int ErrorCode {get;}
public OrderException(string orderId,string message):base(message){OrderId = orderId;}
public OrderException(string orderId,string message,Exception inner):base(message, inner){OrderId = orderId;}
}
// 使用
if(order.Total <0){throw new OrderException(order.Id,"订单金额不能为负数");}

什么时候不需要自定义?

  • 已经有现成的异常类型(ArgumentException、InvalidOperationException等)

  • 只是简单包装一下,没有额外信息

  • 只在内部使用,不对外暴露


五、全局异常处理

Web API全局处理

// .NET Core Web API 全局异常处理
public class GlobalExceptionFilter:IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger){_logger = logger;}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception,"请求处理异常");
var result =new{
            Success =false,
            Message ="系统开小差了,请稍后重试",
            TraceId = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier
};
context.Result =new JsonResult(result){StatusCode =500};
context.ExceptionHandled =true;}
}
// 注册
services.AddControllers(options =>{options.Filters.Add<GlobalExceptionFilter>();});

控制台/服务全局处理

// 全局未处理异常捕获
AppDomain.CurrentDomain.UnhandledException +=(sender, e)=>{
var ex = e.ExceptionObject asException;
Log
.Fatal(ex,"发生未处理异常");
// 发送告警
AlertService
.SendAlert($"程序崩溃:{ex?.Message}");
// 等待日志写入
Thread
.Sleep(3000);
};
Task Scheduler.UnobservedTaskException +=(sender, e)=>{
Log
.Error(e.Exception,"Task未观察异常");
e
.SetObserved();
// 标记为已处理
};

六、日志记录的最佳实践

结构化日志(推荐Serilog)

// 配置
Log.Logger =new LoggerConfiguration().WriteTo.Console().WriteTo.File("logs/log-.txt",rollingInterval: RollingInterval.Day).WriteTo.ApplicationInsights(telemetryClient, TelemetryConverter.Traces).CreateLogger();
// 使用
try
{
// 业务代码
}
catch(Exception ex)
{
Log
.Error(ex,"处理订单失败,订单ID:{OrderId}", orderId);
// 结构化日志会记录OrderId字段,方便查询
}

日志级别怎么选?

级别
使用场景
例子
Trace
调试用,生产环境关掉
方法进入/退出
Debug
开发调试
变量值、中间结果
Information
正常流程记录
用户登录、订单创建
Warning
异常但不影响流程
重试、降级、配置缺失
Error
错误但程序还能跑
数据库连接失败、第三方接口报错
Critical
系统要挂了
内存爆了、磁盘满了

七、异常处理的6个实战技巧

技巧1:Try-Pattern避免异常

// ❌ 用异常
public User GetUser(int id){
try
{
return dbContext.Users.Find(id);
}
catch(Exception)
{
return null;
}
}

// ✅ Try-Pattern
public bool TryGetUser(int id,outUser user){
try
{
user 
= dbContext.Users.Find(id);
return user !=null;
}
catch
{
user =null;
return false;
}
}
// 使用
if(TryGetUser(1001,outvar user)){// 处理用户}

技巧2:异常过滤(Exception Filters)

// 只在特定条件下捕获异常
try
{
awaitCallExternalApi();
}
catch(HttpRequestException ex)
when(ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// 限流了,等会重试
await Task.Delay(5000);
return await Retry();
}
catch(HttpRequestException ex)
when(ex.StatusCode >=500)
{
// 服务端错误,记录日志但不重试
Log
.Error(ex,"外部服务异常");
throw;
}

技巧3:重试模式

public async Task<T>RetryAsync<T>(Func<Task<T>> action,int maxRetries =3){
int retryCount =0;
while(true){
try{return await action();}catch(Exception ex)when(IsTransient(ex)){retryCount++;if(retryCount >= maxRetries)throw;
// 指数退避
var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount));
await Task.Delay(delay);
}
}
}
private bool IsTransient(Exception ex){return ex isTimeoutException|| ex isSqlException sqlEx && sqlEx.Number ==-2|| ex isHttpRequestException httpEx &&(int)httpEx.StatusCode >=500;}

技巧4:断路模式

public class CircuitBreaker{
private int _failCount;
private DateTime _openTime;
private readonly int _threshold =5;
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(30);
public async Task<T>ExecuteAsync<T>(Func<Task<T>> action){
if(_failCount >= _threshold){
if(DateTime.Now - _openTime < _timeout)throw new CircuitBreakerOpenException("服务暂时不可用");
// 半开状态,尝试恢复
​_failCount 
=0;
}
try{
var result =await action();
_failCount =0;
return result;
}
catch
{_failCount++;
if(_failCount >= _threshold)_openTime = DateTime.Now;
throw;
}
}
}

技巧5:Fallback降级

public async Task<User>GetUserAsync(int id){
try{
// 先查缓存
return await _cache.GetAsync<User>($"user:{id}");
}
catch(CacheException)
{
// 缓存挂了,直接查数据库
var user =await _db.GetUserAsync(id);
// 异步尝试恢复缓存(不等待)
= Task.Run(async()=>{
try{
await _cache.SetAsync($"user:{id}", user);
}
catch
{
// 缓存还是不行就算了
}
});
return user;
}
}

技巧6:上下文传递

public class ExceptionWithContext:Exception{
public Dictionary<string,object> Context {get;}=new();
public ExceptionWithContext(string message):base(message){}
public ExceptionWithContext Add(string key,objectvalue){
Context[key]=value;
return this;
}
}
// 使用
throw new ExceptionWithContext("订单处理失败")
.Add("OrderId", orderId)
.Add("UserId", userId)
.Add("Total", order.Total);

八、性能考量:异常真的慢吗?

异常的性能开销

// 测试结果(10万次操作)// 普通返回:0.5ms// 抛出异常:1200ms(慢了2000多倍!)// 所以:不要用异常做流程控制!

异常开销大的原因

  1. 收集调用堆栈

  2. 查找catch块

  3. 展开堆栈

  4. 执行finally


九、团队规范:异常处理检查清单

代码审查时问这几个问题:

  • 这个异常真的能被处理吗?

  • 捕获异常后做了什么?

  • 原始异常信息保留了吗?

  • 日志记录足够定位问题吗?

  • 有没有用异常控制流程?

  • finally释放资源了吗?

  • 自定义异常有必要吗?

十、记住这3句话

  1. 只捕获能处理的异常(处理不了就往上抛)

  2. 保留原始异常信息(别把堆栈弄丢了)

  3. 日志要能定位问题(不然等于没记)


该文章在 2026/2/26 11:01:27 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved