第7章 异常

第7章 异常

7.1 抛出异常

  • DO​​​:操作失败应该通过 抛出异常 的方式报告,而非通过 返回错误码

  • CONSIDER​:在代码遇到严重问题且无法继续安全地执行时,要调用 System.Enviroment.FailFast ​ 终止进程,而不是抛出异常。

    该方法会向 Windows 应用程序事件日志写入消息,然后在发往 Microsoft 的错误报告中加入该消息和可选异常信息。

    Enviroment.FailFast("发生了一个无法挽回的异常", ex)
    

  • CONSIDER​​:抛出异常可能会对性能造成的影响。如果可以,支持 Tester-DoerTry-Parse 模式,避免抛出异常。
    对大多数程序来说,每秒抛出 100 个异常很可能会严重影响程序性能。
    为了避免触发异常,我们可以提供一些方法让用户做前置检查,这样可以防止触发异常:

    ICollection<int> collection = ...
    if (!collection.IsReadOnly) {
        collection.Add(additionalNumber);
    }
    

  • DO​​​:为所有的异常撰写文档,并将其作为契约的一部分。

    作为契约的一部分,也就意味着它不应该随版本而变化(既不应该改变 异常类型 ,也不应该 增加新的异常 )。

  • DON'T ​​​:抛出异常 不应该 出现可选项。

    // 垃圾设计
    public Type GetType(string name, bool throwOnError)
    

  • DON'T ​​​:异常 不能 作为返回值或 out 参数。

    // 不好的设计
    public Exception DoSomething() { ... }
    

  • CONSIDER ​: 使用 辅助方法来创建异常。

    从不同地方抛出同一个异常很常见,为了避免代码重复,可以使用辅助函数来抛出异常。

    此外,抛出异常的成员无法被 内联 ,把抛出异常的语句放在辅助函数中,那么该成员就有可能被 内联

    class File{
        string fileName;
    
        public byte[] Read(int bytes) {
            if(!ReadFile(handle, bytes)) ThrowNewFileIOException(...);
        }
    
        void ThrowNewFileIOException(...){
            string description = // 创建本地字符串
            throw new FileIOException(description);
        }
    }
    

  • DON'T:不要 从异常过滤块中抛出异常。

    例如,以下代码过滤程序判断异常信息中是否包含“foo”,如果 ex.Message​ 为 null​,when​ 语句会抛出异常,CLR 会将其识别为 false​,从而跳过异常处理。

    try{
        // 调用可能抛出异常的方法
    }
    catch (Exception ex) when (ex.Message.Contains("foo")){
        // 处理包含foo的异常
    }
    

  • AVOID:不要 显式地从 finally 代码块中抛出异常。
    在 finally 中抛出异常可能导致以下问题:

    • 如果 try-catch 中已经抛出异常,finally 中抛出的异常会 覆盖掉 之前的异常,导致原始异常信息 丢失
    • 如果 try-catch 中包含 return 语句,finally 中抛出异常会使 return 语句 失效 ,导致方法 提前终止 ,违背程序员预期。

    finally 中隐式地抛出异常,即在调用其他方法时由其他方法抛出异常, 是可以 接受的。

7.2 为抛出的异常选择合适的类型

异常是我们碰到错误时抛出的。错误一般分为两类:

  • 使用 错误

错误调用导致的错误,例如传入了 null 参数。此类错误不应该由框架处理,而应该修改调用方代码。

  • 执行 错误
    又分为两类:

  • 程序 错误

可以在程序中处理的错误。如 `File.Open`​ 未找到相应文件抛出 `FileNotFoundException`​ 异常,我们可以创建一个新文件并继续运行。
  • 系统失败
无法在程序中进行处理的执行错误。如即时编译器(Just-In-Time compiler)用尽了内存而引发的 `OutOfMemoryException`​​​。

  • DON'T ​​​​:使用错误​​​​ 不应该 抛出自定义异常, 应该 抛出框架中已有的异常。

    因使用错误而抛出的异常,应该侧重于写好异常消息(比如在异常消息中给出详尽的解释),并使用.NET 框架中已有的那些异常类型。

  • CONSIDER ​​​:程序错误​​​ 应该 抛出自定义异常。

    前提是它的处理方式和其他异常的处理方式 有所不同 。否则应该抛出已有异常。

  • DON'T ​​​​:如果错误的处理方式和框架中已有异常并没有什么不同, 不要 自定义异常。如果不能通过框架中已有的异常来传达该错误, 自定义异常。不要仅仅为了拥有自己的异常而创建并使用新的异常。

    这种情况应该抛出框架中已有的异常。

    注意:在代码中抛出原异常的派生异常 不会 破坏已有代码。

  • AVOID​:避免设计出会导致系统失败的 API。如果有此类失败,则应该调用 Enviroment.FailFast ​,而不是抛出异常。

  • DO​:使用合理的、最具针对性的异常。

    使用最贴切的异常。

    例如:传入的参数是 null,应该抛出 ArgumentNullException ​,而非其基类 ArgumentException ​。

7.2.1 Exception.Message​ 的设计

  • DO​:抛出异常时应该为开发人员提供丰富而有意义的 错误消息 ,且语句 通顺清晰

    消息应该解释异常产生的原因,并告知用户应该 怎样避免该异常 。并且要做到直接展示给最终用户看也没有问题。

  • DO​​​:异常消息中的每个句子都要有 号,不要包含 号和 号。

  • DON'T​​​​:除非得到许可,否则不要在异常消息中 泄露安全信息

  • CONSIDER​​​:如果有 多语言 需求,异常消息应进行本地化。

7.2.2 异常处理

  • DON'T ​: 框架 ​代码不应该发生吞异常。

    吞异常,指捕获具体类型不确定的异常,并且不对异常进行处理,任由程序继续执行。

    try{
        File.Open(...);
    }
    catch (Exception e) { }  // 此处吞掉了所有异常。不要这么做!
    

    如果是为了把异常转移到另一个线程,则可以进行“吞异常”的操作,且这不会认为是“吞异常”。注意:转移异常后,另外一个线程应处理该异常,否则也是“吞异常”!

  • AVOID​: 应用程序代码 要避免发生吞异常。

    有时在 应用程序 中吞异常是可以接受的。

    吞异常并继续运行,和正常程序相比会有状态不一致的风险,进而导致奇怪的错误(如 OutOfMemoryException​、StackOverflowException​ 或 ThreadAbortException​)。

  • DON'T​​​: 转移异常 的 catch 块,不要剔除任何特定异常。

    catch (Exception e){
        // 错误代码,不要这样做!
        if (e is OutOfMemoryException || e is AccessViolationException)
            throw;
        ...
    }
    

  • CONSIDER​​​​:如果确实了解该异常产生的原因,并能对错误做出适当的响应,应该 捕获该异常 。否则应该 允许该异常沿着调用栈向上游传递

    每编写一个异常处理程序,都有可能导致 bug 被隐藏,进而削弱系统的稳固性,因此请勿滥用。

  • DO​​:进行清理工作时 应该 使用 try-finally,而 try-catch。

    清理工作通常是释放已分配的资源,而异常抛给上一层处理。另外,不要在 catch 块中清理代码。

    另外,C#有 using 语句,可以不用 finally 进行清理。

  • DO​:捕获并重新抛出异常时应该使用 空的 throw 语句。

    这样可以保持调用栈不变。

    try{
        // do something
    }
    catch(Exception e){
        // 上层捕获的异常信息,会包含异常抛出的原始位置。
        throw;
        // 上层捕获的异常信息,会指出此处是异常抛出的原始位置。
        throw e;
    }
    

  • DO​:当从不同的环境中重新抛出异常时,要使用 ExceptionDispatchInfo ​。

    当从其他线程转移异常,或者 catch 后未使用空的 throw 语句再次抛出异常,要使用 ExceptionDispatchInfo ​ 类,它会在重新抛出的过程中持续保存调用栈。

    private ExceptionDispatchInfo _savedExceptionInfo;
    
    private void BackgroundWorker() {
        try{
            ...
        } catch (Exception e){
            _savedExceptionInfo = ExceptionDispatchInfo.Capture(e);
        }
    }
    
    public object GetResult() {
        if(_done) {
            if(_savedExceptionInfo != null){
                _savedExceptionInfo.Throw();
                // 编译器无法理解该方法是抛出了一个异常,因此需要额外的return语句。
                return null;
            }
        }
    }
    

  • DON'T​​​​:不符合 CLS 标准 的异常(非 System.Exception​ 派生异常),不要使用无参数的 catch 块处理。

    经过 CLR2.0 的修改,不符合要求的异常会包装为 RuntimeWarppedException​​ 再抛出。因此可以改为捕获 RuntimeWarppedException​​。

7.2.3 封装异常

  • CONSIDER​:如果低层异常对用户没有意义,高层应该 捕获后将其封装成有意义的异常再抛出 ;如果用户想查看低层异常,则不要 对其进行封装

    • 当然,这么做会抹去异常发生的原始位置。只有当原来的异常几乎没有什么意义,对调试也没有帮助时,才应该这么做。

      try{
          // 读取transaction文件
      }
      catch (FileNotFoundException e) {
          throw new TransactionFileMissingException(..., e);
      }
      

  • AVOID​:类型不确定的异常不应该 二次封装

    这是“吞异常”的另一种形式。

    不过也有例外,当发生了极其严重的错误,其重要性已经超过让调用者知道异常的具体类型,可以对其二次封装。

    例如 TypeInitializationException对静态构造函数中引发的所有异常都进行了封装。

  • DO​​​:封装异常时应为其指定 内部异常(InnerException

    • throw new ConfigurationFileMissingException(..., e);
      

7.3 标准异常类型的使用

7.3.1 Exception​ 与 SystemException

  • DON'T:不要 抛出 System.Exception​ 或 System.SystemException​ 异常。
  • DON'T:不要 在框架代码中捕获 System.Exception​ 或 System.SystemException​ 异常。
  • AVOID​:除非在 程序顶层 ,不要捕获 System.Exception​ 或 System.SystemException ​异常。

7.3.2 ApplicationException

  • DON'T:不要 抛出 System.ApplicationException ​及其子类

    设计之初,SystemException​ 的派生类用于表示 CLR(或系统)自身抛出的异常,ApplicationException​ 的派生类用于表示非 CLR 异常(应用程序异常)。但是很多异常类没有遵循这一模式,如 TargetInvocationException​ 派生自 ApplicationException​,却由 CLR 抛出。因此 ApplicationException​ 已失去原有意义。

7.3.3 InvalidOperationException

  • DO​​:如果对象处于 不正确的状态 ,抛出该异常。

    例如:往只读的 FileStream​​ 写入数据。

7.3.4 ArgumentException​、ArgumentNullException​、ArgumentOutOfRangeException

  • DO​:用户传入 错误参数 时,要抛出 ArgumentException​ 或其派生类,并设置 ParamName ​ 属性。

    如果可以,尽量选择位于继承层次 末尾 的异常类型。

  • DO​:对于属性的 setter​,若要抛出 ArgumentException​ 异常及其派生类,ParamName​ 应该赋值为 value

    public FileAttributes Attributes{
        set {
            if (value == null) {
                throw new ArgumentNullException("value", ...);
            }
        }
    }
    

7.3.5 NullReferenceException​、IndexOutOfRangeException​ 及 AccessViolationException

  • DON'T​:公共 API 不应该抛出上述异常。这些异常应该由 执行引擎抛出 ,表示 代码存在缺陷

    做好参数检查避免抛出这些异常。这些异常会暴漏方法的实现细节,而这些细节可能会随着时间而改变。

7.3.6 StackOverflowException

该异常会在无限递归中发生

  • DON'T​​:只有 CLR 才能抛出该异常,自己编写的代码不应该抛出该异常。

  • DON'T:不要 捕获该异常。

    栈溢出时,几乎不可能让托管代码保持状态一致。发生该异常 CLR2.0 默认会让程序立即终止。

7.3.7 OutOfMemoryException

该异常会在分配内存失败时发生

  • DON'T​​:只有 CLR 才能抛出该异常,自己编写的代码不应该抛出该异常。

7.3.8 ComException​、SEHException​ 以及 ExecutionEngineException

  • DON'T​​:只有 CLR 才能抛出该异常,自己编写的代码不应该抛出该异常。

ComException
用于表示 COM 组件或 Windows API 返回未知的 HRESULT 而抛出的异常。

SEHException
用于表示 C++ 或 Windows API 中未映射到.NET Framework 的异常。

ExecutionEngineException
用于表示致命的执行引擎错误。通常由内存损坏、堆栈溢出、访问冲突或其他严重问题导致。

7.3.9 OperationCanceledException​ 和 TaskCanceledException

  • DO​:要抛出 OperationCanceledException​ 来表明调用者发起了 中止取消 操作。

  • DO​​:要倾向于捕获 OperationCanceledException ​​,而非 TaskCanceledException ​​。

    TaskCanceledException​ ​派生自 OperationCanceledException​,由 Task 执行引擎内部使用。需要对 TaskCanceledException​ ​进行特殊处理的 catch 块一般也处理 OperationCanceledException​。

7.3.10 FormatException

  • DO​:要抛出​该异常​来表明文本解析方法中的输入字符串不符合要求或指定格式。

  • DO​:要抛出 FormatException ​ 来表明文本解析方法中或文本格式化方法中用于限定格式的字符串是无效的。

7.3.11 PlatformNotSupportedException

  • DO​​:要抛出该异常来表明 在当前的运行时环境下 无法完成操作,但在不同的 运行时或操作系统上 可以完成。

7.4 自定义异常的设计

  • DO​:自定义异常应该派生自 System.Exception ​ 或其他 常用的基类异常 ,且继承层次不应该 过深 ,命名使用 “Exception” 后缀。如果多种错误 可以通过一种方式来处理 ,则它们应该属于同一类型的异常。

  • DO ​:异常 要可以 序列化。

    • 便于跨应用程序域、跨远程边界时仍可以使用。

  • DO​​:异常(至少)要有如下构造函数:

    public class SomeException : Exception, ISerializable {
        public SomeException();
        public SomeExcepiton(string message);
        public SomeExcepiton(string message, Exception inner);
    
        // 序列化所需构造函数
        protected SomeException(SerializationInfo info, StreamingContext context);
    }
    

  • DO:应该覆写 ToString 用于报告与异常相关的信息。如果该信息与安全性相关,该信息应该保存在私有成员中,并确保只有可信赖的代码才能获得该信息。
    注意:如果没有获得许可,不要泄露安全信息。

    第三版移除此准则。

  • CONSIDER​​​:可以为异常定义属性,用于获取除 message 以外的额外信息。

7.5 异常与性能

我们可以通过 Tester-Doer 模式、Try-Parse 模式提高性能。不要因为影响性能而回归错误码。

7.5.1 Tester-Doer 模式

  • CONSIDER​​:使用 Tester-Doer 模式避免 因异常引起的性能 问题。

    • ICollection<int> numbers = ...
      ...
      if(!numbers.IsReadOnly) {
          numbers.Add(1);
      }
      

      其中 numbers.IsReadOnly​ 成员是“tester”,numbers.Add(1)​ 成员是“doer”。

      能这么做的前提是框架提供了“tester”。

注意:该模式要小心多线程访问造成的竞态条件。

7.5.2 Try-Parse 模式

  • CONSIDER​​​:如果成员在 常用代码中 都可能抛出异常,应使用 Try-Parse 模式避免因异常引起的性能问题。

  • DO​​:Try-Parse 模式要使用“ Try ”前缀,用 bool 作为返回类型。

  • DO​​​​:Try 方法返回 false​ 的原因只有一种,其余类的失败则要 抛出异常

    调用该方法的开发者可以为该失败情况写一个处理程序,并理解它所打标的状态。同时,对哪些不太可能的错误,继续抛出异常。

  • DO​:要为 Try 方法提供 等价抛出异常 的方法。

    public struct DateTime {
        public static DateTime Parse(string dateTime) { ... }
        public static bool TryParse(string dateTime, out DateTime result) { ... }
    }
    

7.5.2.1 Try 方法的值生成

  • DO​​:要通过 out 参数 ,返回 Try 方法的值。

  • DO​:要在 Try 方法返回 false 时,将 default(T) 赋值给 out 参数。

    public partial class HashSet<T> {
        public bool TryGetValue(T equalValue, out T actualValue) {
            Node node = FindNode(equalValue);
            if (node != null) {
                actualValue = node.Item;
                return true;
            }
            acutalValue = default(T);
            return false;
        }
    }
    

  • AVOID​​:避免在 抛出异常时 向 Try 方法的 out 参数写入数据。

7.5.2.2 静态的 TryParse(string, out T)方法

  • DO​​:对于静态的 TryParse(string, out T)​​​方法,若输入的是空字符串,要 返回 false

    NET BCL 类型中所有静态 TryParse​ 都将空输入视为“非有效字符串”,而不是异常。为保持一致性,我们的静态 TryParse 方法也要遵循该准则。

posted @   hihaojie  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤
点击右上角即可分享
微信分享提示