.NET异常处理

发表于:2020-9-01 10:25

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:JameLee    来源:掘金

  .NET 提供了一种统一的方式来报告应用程序的错误,即通过引发异常来指示具体问题。这相比于 Win32 时代的错误处理(通过 GetLastError 或者 HRESULT 的方式 ),不但要简单明了得多,还更容易维护。通过监控程序可能引发的异常,并对异常做出相应的处理(比如数据恢复、日志记录),可以提高程序的可靠性、可维护性。
  对异常的理解
  异常,即程序在运行过程中,遇到的错误或意外的行为。
  在 .NET 中,异常都是从 System.Exception 类继承。具体的异常由发生问题的代码引发,然后它在堆栈中向上传递,直到应用程序对其进行处理或者程序终止为止。
  Exception 定义如下:
  public class Exception : ISerializable, _Exception {
      public Exception();
      public Exception(string message);
      public virtual string Source { get; set; }
      public virtual string HelpLink { get; set; }
      // 包含可用于确定错误位置的堆栈跟踪
      // 如果有可用的调试信息,则堆栈跟踪包含源文件名和程序行号
      public virtual string StackTrace { get; }
      public MethodBase TargetSite { get; }
      public Exception InnerException { get; }
      // 一般情况,我们可以通过这个属性,来理解具体的异常
      public virtual string Message { get; }
      public int HResult { get; protected set; }
      public virtual IDictionary Data { get; }
  }
  常见的异常有 IndexOutOfRangeException、NullReferenceException、ArgumentNullException 等等。
  每一种异常,都对应于一个特定的情形。在实际项目中,如果需要自定义异常,也应该遵循这个原则。比如 IndexOutOfRangeException 针对的就是数组或集合访问越界的情形。
  使用 try...catch 块捕获异常
  对于有可能产生异常的代码,我们可以使用 try...catch 将这些代码包围起来,并在 catch 子句中指明需要捕获的异常,同时可以在 catch 的代码块内,对该异常做出相应的处理。
  一般情况下,我们应该在 catch 子句中指明具体的异常,因为这样我们就能很方便地对不同的异常做出具体的处理,以便程序尽可能的从异常中恢复过来。比如像下面这样:
  try {
      using (StreamReader sr = File.OpenText("data.txt")) {
          Console.WriteLine($"First Line: {sr.ReadLine()}");
      }
  } catch (UnauthorizedAccessException e) {
      // 针对未授权做出处理。比如请求管理员权限
  } catch (DirectoryNotFoundException e) {
      // 针对目录不存在处理。比如弹窗提示用户
  } catch (FileNotFoundException e) {
      // 针对文件不存在处理。比如弹窗处理
  }
  当然,如果我们仅仅是为了捕获异常,并记录日志。此时我们只需要在 catch 子句中,使用 Exception 即可(Exception类为所有异常类的基类),如下:
  try {
      using (StreamReader sr = File.OpenText("data.txt")) {
          Console.WriteLine($"First Line: {sr.ReadLine()}");
      }
  } catch (Exception e) {
      // 这种情况下,我们可以通过日志记录的模块,来记录此次异常
      // 以便后期维护使用
  }
  如果我们在处理了特定的异常之后,希望将其他意料之外的异常也记录在日志中。
  需要特别注意的是:Exception 只能放置在最后的 catch 子句中,因为它是其他异常的基类。
  示例如下:
  try {
      using (StreamReader sr = File.OpenText("data.txt")) {
          Console.WriteLine($"First Line: {sr.ReadLine()}");
      }
  } catch (UnauthorizedAccessException e) {
      // 针对未授权做出处理。比如请求管理员权限
  } catch (DirectoryNotFoundException e) {
      // 针对目录不存在处理。比如弹窗提示用户
  } catch (FileNotFoundException e) {
      // 针对文件不存在处理。比如弹窗处理
  } catch(Exception e) {
      // 通过日志记录的模块,记录意料之外的异常
  }
  因此,有以下结论:在 catch 子句中,子类应该放在其父类的前面。否则,将无法捕获具体子类的异常。比如前面的这段代码,如果我们将 Exception e 放置在其他的异常之前,那么其他异常的具体处理逻辑都无法执行。
  我们应该如何引发异常
  我们可以使用 throw 语句显式引发异常。使用方式有两种:
  方式一
  throw 异常对象的方式。比如,我们可以通过 throw new ArgumentNullException() 来引发一个参数为空的异常;也可以使用 throw new Exception() 来引发,不过不建议这样使用。使用特定情形的异常对象是最好的做法。
  使用方式如下:
  public void ProcessData(int[] source, int from, int count) {
      if (source == null)
          throw new ArgumentNullException("source", "The source you provided cannot be null");
      if (from < 0 || from >= source.Length)
          throw new IndexOutOfRangeException("The 'from' parameter is out of range");
      // ...
      // 其他逻辑
      // ...
  }
  这种引发异常的方式,在我们的写公共的类库的时候会经常用到。因为在公共类库中,我们需要将发生问题的详细信息传递出去,以方便使用类库的开发者调试。
  方式二
  直接使用 throw; 的方式,这种使用方式只能存在于 catch 子句中。当我们在处理了具体的异常之后,仍然希望上层能够捕获此异常的时候非常有用。
  使用方式如下:
  try {
      // 业务逻辑代码
  } catch (UnauthorizedAccessException e) {
      // 异常处理逻辑
      // 将异常传递出去
      throw;
  }
  创建自定义异常
  在预定义的异常不符合业务需求的情况下(比如预定义异常无法携带我们需要的信息时),我们可以通过从 Exception 类派生来创建自己的异常类。
  比如,我们需要一个数据库中用户不存在的异常,则可以按如下方式处理:
  public class UserNotExistException : Exception {
      public string UserName { get; }
      public string UserId { get; }
      public UserNotExistException(string userName, string userId) {
          this.UserName = userName;
          this.UserId = userId;
      }
  }
  在用户不存在的情况下,通过引发此 UserNotExistException 异常,我们可以很容易的获取到不存在的用户的ID及昵称。
  使用 finally 块
  定义在 finally 块中的代码,其表示:无论 try 块中是否有异常发生,都会执行。常见于资源的清理,比如文件操作、网络操作或数据库操作完成之后。
  如下代码所示:
  StreamReader sr = null;
  try {
      sr = File.OpenText("data.txt");
      Console.WriteLine($"First Line: {sr.ReadLine()}");
  } catch (UnauthorizedAccessException e) {
  } catch (DirectoryNotFoundException e) {
  } catch (FileNotFoundException e) {
  } catch (Exception e) {
  } finally {
      // 无论前面是否发生异常,我们都需要销毁文件资源 
      if (sr != null) {
          sr.Dispose();
      }
  }
  COM 互操作异常
  一般情况下, 如果因COM方法失败而返回 HRESULT,运行时会将其映射为可由托管代码捕获的异常。例如,E_ACCESSDENIED 将映射为 UnauthorizedAccessException、E_OUTOFMEMORY 映射为 OutOfMemoryException,等等。
  如果 HRESULT 为自定义值,或 CLR 无法将其映射成预定义的具体托管异常。运行时会引发 COMException 异常, 其 ErrorCode 属性包含具体的 HRESULT 值。
  编码建议
  设计良好的异常处理机制可以防止应用崩溃。这部分介绍了在实际项目中处理和创建异常的一些建议:
  ·合理使用 try...catch,过于频繁的使用,会造成性能低下(如果某些异常一直出现)。况且,我们也不应该过于依赖异常处理机制,对业务逻辑中具体情况进行良好的处理,比用 try...catch 更有意义。比如,当我们尝试关闭已关闭的连接时,就会引发 InvalidOperationException 异常。为了避免这个异常,我们可以在尝试关闭前,通过使用 if 语句检查连接状态,避免该情况
  ·对于某些情况,如果在返回 null,或者类型的默认值的情况下,不会影响对方法的理解。那么我们应该返回类型的默认值或null,而不是去引发一个异常
  ·如果可以不引发异常,那么就不引发异常。这时我们只需要在程序中对特定的情况进行处理修正即可。一般情况下,如果引发异常无法带来好处,或者并没有让我们提供的接口、方法等更易于理解,那就没必要引发异常
  ·仅在异常需要携带某些自定义数据的情况下,去自定义异常(该异常类应该以 Exception 结尾)。否则,我们使用系统预定义异常即可
  ·在每个异常中都包含一个本地化描述字符串。一般情况下,如果不是跨国籍合作,我们可以都使用中文,或者一律使用英文,也可以混搭,这个根据公司项目的情况而定
  在实际项目中,我们应该参考上面的建议,以帮助我们写出性能和可维护性都较好的代码。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号