C#中try-catch的核心目的是精准识别异常类型、定位错误场景并决定恢复或上报策略,而非防止程序崩溃;滥用catch(Exception)会掩盖问题本质,正确做法是按具体到宽泛顺序捕获预期异常,用throw;保留原始堆栈,finally仅用于释放已成功获取的非托管资源。

直接说结论:C# 中写 try-catch 不是为了“让程序不崩”,而是为了「明确知道谁可能出错、错在哪类场景、后续怎么恢复或上报」。随便套个 catch (Exception e) 反而会让问题更难定位。
为什么 catch (Exception) 是最常踩的坑
常见错误现象是把所有异常都塞进同一个 catch (Exception e) 里,然后打个日志就完事。结果 NullReferenceException 和 FileNotFoundException 全被当成“普通错误”吞掉,根本分不清是代码逻辑空指针,还是配置路径写错了。
- 真正该捕获的是你**预期中可能发生**的异常,比如读文件时用
catch (FileNotFoundException e),反序列化时用catch (JsonException e) -
catch (Exception)只应在最外层兜底,且必须配合throw;或记录完整上下文(如e.Source、e.Data) - 如果在业务方法里写了
catch (Exception) { /* 静默 */ },等于主动关闭了调试线索
多个 catch 块必须按从具体到宽泛排序
编译器会严格检查 catch 顺序。比如 ArgumentNullException 继承自 ArgumentException,那前者必须写在后者前面,否则后者的块永远进不去,编译直接报错。
- 正确顺序示例:
catch (UnauthorizedAccessException)→catch (IOException)→catch (Exception) - 错误写法:
catch (Exception)写在第一个——后面所有具体类型都会被它提前截获,编译通不过 - 别依赖“反正最后有个 Exception 兜底”就乱排,顺序本身就是在表达你的处理意图
throw; 和 throw e; 的区别不是语义差别,是调试生死线
throw; 保留原始堆栈,throw e; 把堆栈重置成当前 catch 块的位置。这意味着你看到的异常行号,可能是日志打印那一行,而不是真实出问题的第 12 行。
- 需要原样上抛?只用
throw;(不带参数) - 要包装异常并保留根因?用
throw new CustomException("msg", e);,且必须传入e作为InnerException - 绝对不要写
catch (Exception e) { throw e; }——这是主动擦除调用链
finally 不是用来写日志或弹提示的
finally 的唯一正当用途,是释放**已经成功获取的非托管资源**,比如 FileStream、SqlConnection、Graphics 对象。它不保证资源一定存在,也不适合做业务收尾。
- 如果
new FileStream(path)在 try 块里就抛了ArgumentException,那 finally 里调stream?.Close()会触发NullReferenceException - 优先用
using语句替代手写finally,它能安全处理构造失败的情况 - 别在
finally里调 API 或写数据库——万一它也抛异常,会覆盖原始异常,彻底丢失根因
最易被忽略的一点:异常不是流程控制手段。用 try-catch 去判断文件是否存在、去检测对象是否为 null、去替代 if (dict.ContainsKey(key)),性能差且语义混乱。该用防御性编程的地方,别偷懒扔给异常机制扛。


















