C# 语言之异常处理

错误和异常

在日常的开发中,由于各种不确定因素,代码编译或程序运行可能会出现各种错误,比如:编译错误、使用错误、程序错误、系统错误等。但是,并非所有错误都需要在代码中做异常处理,下面是一些可能在编码或运行时出现的错误以及相应的处理方法:

  • 编译错误:编译错误表示在编码过程做编写了错误代码导致编译器编译不通过,通常编译器(vs)会用红色下划线标注,这类错误最明显,我们只需要根据编译器提示的错误信息去修改便可。

  • 逻辑错误:逻辑错误表示代码编译正常,但是代码处理逻辑出现了问题,导致程序执行产生了不确定(错误)的结果。像这样的情况,我们可以重新修正逻辑代码去规避错误的出现。我们来看下面的例子:

    我们想实现的例子是,在 Person 类中重写 Equals 方法来比较不同的 Person 实例是否相等。

  1. public class Person
  2. {
  3. private string _name;
  4. public string Name
  5. {
  6. get { return _name; }
  7. set { _name = value; }
  8. }
  9. public override bool Equals(object obj)
  10. {
  11. Person p = (Person) obj;
  12. return this.Name.Equals(p.Name);
  13. }
  14. }
  15. public class Example
  16. {
  17. public static void Main()
  18. {
  19. Person p1 = new Person();
  20. p1.Name = "John";
  21. Person p2 = null;
  22. Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
  23. }
  24. }

在上面的示例中,程序执行到 p1.Equals(p2)=> Person p = (Person) obj; 时会出 NullReferenceException 异常,因为 objnull 对象。

我们知道了异常出现的原因,可以对上面的代码做出修改,以此来规避异常的出现:

  1. public override bool Equals(object obj)
  2. {
  3. Person p = obj as Person;
  4. if (p == null)
  5. return false;
  6. else
  7. return this.Name.Equals(p.Name);
  8. }
  • 程序错误:程序错误与上面的逻辑错误类似,都是在程序运行时产生的异常以及一些无法预料的结果。例如打开一个不存在的文件 File.Open("XXX", FileMode.Open); 当我们不确定文件是否存在时,我们应该先判断文件是否存在,如果存在才执行打开操作:File.Exists("XXX");。还有在日常开发最容易出现的数据类型转换等。

  • 系统错误:系统错误同样是一个在程序运行时出现,切无法通过修改代码的方式来规避的错误。 例如,OutOfMemoryException 如果公共语言运行时无法分配更多内存,则程序中的任何方法执行都有可能引发该异常。

Try/catch 块

公共语言运行时提供的异常处理模型,该模型将程序代码异常处理代码try 块和 catch 块分离。可以有一个或多个 catch 块,每个块用于处理特定类型的异常。

在实际使用中,通常将无法预知结果可能出现异常的语句放在 try 中,称为 try块;将处理 try块 中语句引发的异常的语句放入 catch 中,成为 catch块。通俗点说就是使用 try 将正常的代码块包裹,在 catch 中编写异常处理逻辑。

  1. try
  2. {
  3. // to do
  4. }
  5. catch(Exception ex)
  6. {
  7. // exception to do
  8. }

如果try块中的代码发生异常,系统会按照代码异常在应用程序中出现的顺序搜索关联的catch块,直到找到对应的 catch块。系统在找到处理异常的第一个块后停止搜索 catch块

如果系统无法找到对应的 catch块,并且当前 try块 嵌套在当前调用的其他 try 块中,则搜索与下一个封闭块关联的 catch块 ,如果仍未找到异常的catch块,则系统会在当前调用中搜索先前的嵌套级别。如果在当前调用中找不到异常的catch块,则将在调用堆栈中向上传递异常。

常用的异常类

  • Exception:所有异常类的基类

  • SystemException:System 命名空间中所有其他异常类的基类。(建议:公共语言运行时引发的异常通常用此类)

  • ApplicationException:应用程序发生非致命错误时所引发的异常。(建议:应用程序自身引发的异常通常用此类)

与参数有关的异常类
  • ArgumentException:该类派生于 SystemException,用于处理参数无效的异常,除了继承来的属性名,此类还提供了string 类型的属性 ParamName 表示引发异常的参数名称。

  • FormatException:该类派生于 SystemException,用于处理参数格式错误的异常。

与成员访问有关的异常
  • MemberAccessException:用于处理访问类成员失败时所引发的异常。失败可能的原因是没有足够的访问权限,也可能是要访问的成员根本不存在(类与类之间调用时常用)

  • FileAccessException:该类派生于 MemberAccessException,用于处理访问字段成员失败所引发的异常。

  • MethodAccessException:该类派生于 MemberAccessException,用于处理访问方法成员失败所引发的异常。

  • MissingMemberException:该类派生于 MemberAccessException,用于处理成员不存在时所引发的异常。

与数组有关的异常
  • IndexOutOfException:该类派生于 SystemException,用于处理下标超出了数组长度所引发的异常。

  • ArrayTypeMismatchException:该类派生于 SystemException,用于处理在数组中存储数据类型不正确的元素所引发的异常。

  • RankException:该类派生于 SystemException,用于处理维数错误所引发的异常。

与IO有关的异常
  • IOException:用于处理进行文件输入输出操作时所引发的异常。

  • DirectionNotFoundException:该类派生于 IOException,用于处理没有找到指定的目录而引发的异常。

  • FileNotFoundException:该类派生于 IOException,用于处理没有找到文件而引发的异常。

  • EndOfStreamException:该类派生于 IOException,用于处理已经到达流的末尾而还要继续读数据而引发的异常。

  • FileLoadException:该类派生于 IOException,用于处理无法加载文件而引发的异常。

  • PathTooLongException:该类派生于 IOException,用于处理由于文件名太长而引发的异常。

与算术有关的异常
  • ArithmeticException:用于处理与算术有关的异常。

  • DivideByZeroException:该类派生于 ArithmeticException,表示整数货十进制运算中试图除以零而引发的异常。

  • NotFiniteNumberException:该类派生于 ArithmeticException,表示浮点数运算中出现无穷打或者非负值时所引发的异常。

异常类的基本属性

  1. // 获取或设置导致错误的应用程序或对象的名称。
  2. public virtual string Source { get; set; }
  3. // 获取或设置指向与此异常关联的帮助文件链接。
  4. public virtual string HelpLink { get; set; }
  5. // 获取调用堆栈上的即时框架字符串表示形式。
  6. public virtual string StackTrace { get; }
  7. // 获取引发当前异常的方法。
  8. public MethodBase TargetSite { get; }
  9. // 获取导致当前异常的 System.Exception 实例。
  10. public Exception InnerException { get; }
  11. // 获取描述当前异常的消息。
  12. public virtual string Message { get; }
  13. // 获取或设置 HRESULT(一个分配给特定异常的编码数字值)。
  14. public int HResult { get; protected set; }
  15. // 获取提供有关异常的其他用户定义信息的键/值对集合。
  16. public virtual IDictionary Data { get; }

注意事项

引发或处理异常需要消耗大量的系统资源和执行时间。异常处理一般是作用于一些不可知的情况,而不是处理可知的事件或流控制。例如:我们在封装一些方法时,需要输入参数,如果参数类型不对可能会引发异常。这时,我们要做的不是去怎么处理异常,而是通过断言去修改代码,使其响应正确的结果。

此外,异常处理尽量与 日志 结合使用,要定期检查异常日志,不要让异常长期存在。