C#异常处理需明确捕获目的、重抛时机和异常类型选择:应捕获具体异常而非Exception,多catch按从具体到宽泛排序,用throw;保留堆栈而非throw e;,finally仅用于释放已获取的非托管资源,避免用异常控制流程。

直接说结论:C# 中处理异常不是靠“写完 try-catch 就算完事”,而是要分清「捕获目的」「重抛时机」「异常类型选择」这三件事。盲目 catch Exception 或无脑 throw e; 反而会让错误更难定位。
catch 应该指定具体异常类型,而不是只写 Exception
常见错误现象是把所有异常都用 catch (Exception e) 捕获,结果掩盖了本可预判的问题(比如 NullReferenceException 其实暴露的是空值未校验,FileNotFoundException 暴露的是路径配置错)。
- 优先捕获你明确知道可能发生的异常,例如读文件用
catch (FileNotFoundException e)、转类型用catch (InvalidCastException e) - 多个
catch块时,必须从具体到宽泛排列,否则编译报错 —— 比如catch (ArgumentNullException)必须写在catch (ArgumentException)前面 - 如果真要兜底,也建议在最后加一个
catch (Exception e),但里面至少要记录日志并throw;重新抛出,而不是静默吞掉
throw; 和 throw e; 的区别必须搞清
这是最容易踩的坑:用 throw e; 会重置堆栈跟踪,导致你看到的异常位置是 catch 块内部,而不是原始出错行 —— 调试时直接丢线索。
- 要保留原始堆栈,一律用
throw;(不带参数) - 只有在需要包装异常时才用
throw new CustomException("msg", e);,且必须把原异常e作为InnerException传入 - 别写
catch (Exception e) { throw e; }—— 这等于主动销毁调试信息
什么时候该用 finally,什么时候不该用
finally 不是用来“收尾日志”或“通知用户”的,它的唯一合理用途是释放**确定已成功获取的非托管资源**,比如文件句柄、数据库连接、网络流。
- 如果
try块里new FileStream(...)都没执行成功(比如路径非法抛了ArgumentException),那finally里去.Close()就会空引用 - 推荐优先用
using语句替代手动finally,它会在作用域结束时自动调用Dispose(),且能正确处理构造失败的情况 - 不要在
finally里写可能抛异常的逻辑(比如又去调一次 API),否则会覆盖原始异常
自定义异常不是为了“起个好名字”,而是为了传递上下文
很多人以为继承 Exception 然后改个类名就叫自定义异常,其实关键在构造函数和属性设计。
- 至少提供三个构造函数:无参、带消息、带消息 +
InnerException - 如果业务上有固定字段(比如订单号、用户 ID),就加只读属性,并在构造时赋值,别全塞进
Message字符串里 - 避免用
throw new Exception("xxx")—— 它无法被下游代码区分处理,也违背 .NET 异常分类惯例
最常被忽略的一点:异常处理的成本不在 catch 本身,而在异常实际被抛出的那一刻。频繁靠抛异常来控制流程(比如用 int.Parse + catch 做输入校验)会导致性能断崖式下跌。这种场景该用 int.TryParse。


















