【C# 异常处理】 开端


 异常概述

在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅等等。
设计类型时要想好各种使用情况类型名称通常是名词,例如filestream或者string builder,然后要为类型定义属性、方法、事件等这些成员的定义方式(属性的数据类型、方法的参数、返回的值等)就是类型的编程接口,这些成员代表类本身或者类型实力能执行的行动,行动成员通常动词表示,例如,read,write,flush、Apend、Insert和remove等。当行动成员不能完成任务时,应抛出异常。在 .NET中,异常是从 System.Exception 类继承的对象。 异常引发自发生问题的代码区域。 异常在堆栈中向上传递,直到应用程序对其进行处理或者程序终止。

定义

异常指成员没有完成它的名称所宣称的行动。

异常分类: 托管异常和CLR内部异常(java 中用Error表示)

当我们提到CLR里的“异常”,要注意一个很重要的区别。有通过如C#的try/catch/finally暴露给应用程序,并由运行时提供机制全权实现的托管异常。也有运行时自己使用的异常。大部分运行时开发人员很少需要想到如何实现并暴露托管异常模型。但每个运行时开发人员都应该懂得CLR实现里是怎么使用异常的。为了保持区分,本文将托管程序抛出并捕捉的称为托管异常,而将运行时自己使用的错误处理方式称为 CLR内部异常。本文主要讨论CLR内部异常。

通常说异常指托管异常,CLR内部异常程序无法处理。

避免使用异常的情况

使用错误(未进行null检测)程序错误(输入数字错误)、系统失败(栈溢出)这些都是运行错误。但是要用正常方法处理避免使用异常处理的方式。

异常处理机制Message 包含在返回的信息中 ToString 。 Message仅在创建时设置属性 Exception 。 如果没有向当前实例的构造函数提供消息,则系统提供使用当前系统区域性进行格式设置的默认消息。

void FirstTimeException()
{
    //出现异常
    try {throw new AppException("Base异常"); }
    catch (Exception e) { throw new AppException("Second Exception", e); }//不处理继续网上抛
}
class AppException : Exception
{
    public AppException(string mess) : base(mess) { }
    public AppException(string mess, Exception inner) : base(mess, inner) { }

}

 Exception.InnerException 属性

如果当前异常是在处理一个异常的时候抛出的,那么该属性就指向上个异常。 InnerException 属性返回的值与传递到 Exception(String, Exception) 构造函数中的值相同,如果没有向构造函数提供内部异常值,则为 null此属性为只读。

如果每次都抛出的异常,经过封装后往上抛。那么就会形成链表结构。就可以通过Exception 公共的方法GetBaseException遍历异常构成的链表,并返回最初的异常。

using System.Collections;

PrinteException();
void PrinteException()
{
    try{ThirdTimeException();}
    catch (Exception e) 
    {
        //输出所以异常
        while (e != null)
        {
            Console.WriteLine(e.Message);
            e = e.InnerException;
        } 
    }}

void ThirdTimeException()
{
    try{ FirstTimeException(); }
    catch (Exception e) { throw new AppException("tird Exception", e); }//不处理继续网上抛
}
void FirstTimeException()
{
    //出现异常
    try {throw new AppException("Base异常"); }
    catch (Exception e) { throw new AppException("Second Exception", e); }//不处理继续网上抛
}
class AppException : Exception
{
    public AppException(string mess) : base(mess) { }
    public AppException(string mess, Exception inner) : base(mess, inner) { }

}

Exception 提供了一个公共的方法GetBaseException内层异常异常构成的链表,并返回最初的异常

 public virtual Exception GetBaseException()
        {
            Exception? inner = InnerException;
            Exception back = this;
 
            while (inner != null)
            {
                back = inner;
                inner = inner.InnerException;
            }
 
            return back;
        }

 Exception.StackTrace 属性

该属性依赖于.pdb文件配合。如果没有.pdb文件将无法输出源代码中的文件路径和行号、列号。

 输出的结果取决抛出的方式:throw或throw e

采用了。方法调用的跟踪称为堆栈跟踪。堆栈跟踪列表提供了一种方法,可以跟踪调用堆栈到发生异常的方法中的行号。

StackTrace属性返回在抛出异常的位置产生的调用堆栈的帧。你可以通过创建System.Diagnostics.StackTrace类的新实例,并使用它的【StackTrace.ToString() 】方法来获取调用堆栈中其他帧的信息。

每当在应用程序代码中抛出异常时(通过使用throw关键字),公共语言运行时(CLR)都会更新堆栈跟踪。如果异常在与最初抛出异常的方法不同的方法中被重新抛出,则堆栈跟踪将包含最初抛出异常的方法中的位置,以及重新抛出异常的方法中的位置。如果在相同的方法中抛出异常,然后重新抛出异常,则堆栈跟踪只包含重新抛出异常的位置,而不包含最初抛出异常的位置。

    • 实际是实时访问了CLR中的代码,并且返回一个调用堆栈跟踪的字符串。
      异常抛出时,CLR在内部记录throw指令的位置(抛出位置)。一个catch块捕捉到该异常时,CLR记录捕捉的位置。在catch块内访问被抛出的异常对象StackTrace属性,负责实现该属性的代码会调用CLR内部的代码,返回一个字符串来指出从异常抛出位置到异常捕捉位置的所有方法。
      抛出异常时,CLR会重置异常起点。也就是说,CLR只记录最新的异常对象的抛出位置。(如果同一个exception,catch之后不做处理,再将其抛出,由于是同一个异常的缘故,所以还是可以获得StackTrace信息的)
    • 构造Exception的派生类型时,StackTrace的初值是null。
    • StackTrace属性返回的字符串不包含调用栈中比异常发生出更早的方法。即只包含到了异常发生的地方。
    • 要获得从线程起始处得到异常处理程序之间的完整堆栈跟踪,需要使用System.Diagnostics.StackTrace类型。


StackTrace属性报告的方法调用可能没有预期的那么多:

1、因为在优化过程中会发生代码转换(比如内联)。

2、调用栈记录的是线程的返回位置(而非来源的位置)

使用案例:

using System.Collections;

PrinteException();
void PrinteException()
{
    try{ThirdTimeException();}
    catch (Exception e) 
    {
        Console.WriteLine(e.StackTrace);  
    }}

void ThirdTimeException()
{
    try{ FirstTimeException(); }
    catch (Exception e) { throw new AppException("tird Exception", e); }//不处理继续网上抛
}
void FirstTimeException()
{
    //出现异常
    try {throw new AppException("Base异常"); }
    catch (Exception e) { throw new AppException("Second Exception", e); }//不处理继续网上抛
}
class AppException : Exception
{
    public AppException(string mess) : base(mess) { }
    public AppException(string mess, Exception inner) : base(mess, inner) { }

}

 
/* 输出:
       at Program.<
$>g__ThirdTimeException|0_1() in C:\Users\HP\source\repos\ConsoleApp10\ConsoleApp10\Program.cs:line 15 at Program.<
$>g__PrinteException|0_0() in C:\Users\HP\source\repos\ConsoleApp10\ConsoleApp10\Program.cs:line 6 */

 Exception.Source 属性

包含生成异常的程序集名称

using System.Collections;

PrinteException();
void PrinteException()
{
    try{ FirstTimeException(); }
    catch (Exception e) 
    {
        Console.WriteLine(e.Source);  ///包含生成异常的程序集名称
    }}


void FirstTimeException()
{
    //出现异常
    try {throw new AppException("Base异常"); }
    catch (Exception e) { throw new AppException("Second Exception", e); }//不处理继续网上抛
}
class AppException : Exception
{
    public AppException(string mess) : base(mess) { }
    public AppException(string mess, Exception inner) : base(mess, inner) { }

}

/* 
输出:ConsoleApp10*/

Exception.TargetSite属性

找到最初抛出异常的那个方法

using System.Collections;

PrinteException();
void PrinteException()
{
    try{ FirstTimeException(); }
    catch (Exception e) 
    {
        Console.WriteLine(e.TargetSite);  //Void <
$>g__FirstTimeException|0_1() } } void FirstTimeException() { //出现异常 try {throw new AppException("Base异常"); } catch (Exception e) { throw new AppException("Second Exception", e); }//不处理继续网上抛 } class AppException : Exception { public AppException(string mess) : base(mess) { } public AppException(string mess, Exception inner) : base(mess, inner) { } } /* 输出:Void <

 自定义异常类

现在的实际使用和MSDN文档都建议直接从System.Exception派生自定义的异常。

下面是创建一个自定义异常类型的几个原则:

1,声明序列化,这样可以跨AppDomain访问。
2,添加序列化信息的构造函数,访问级别设为private或protected。

3、Exception()(使用默认值)。
     Exception(String),它接受字符串消息。
     Exception(String, Exception),它接受字符串消息和内部异常。

4、.NET 可提供由基类 Exception 最终派生的异常类层次结构。 然而,如果预定义的异常都不符合需求,可通过从 Exception 类派生来创建自己的异常类。

5、创建自己的异常时,用户定义的异常类的名称需要以“Exception”一词结尾,并实现三个常见的构造函数,如以下示例所示。 该示例定义名为 EmployeeListNotFoundException 的新异常类。 该类从 Exception 派生,且包含三个构造函数。

using System;
[Serializable]
public class EmployeeListNotFoundException : Exception
{
    public EmployeeListNotFoundException()
    {
    }

    public EmployeeListNotFoundException(string message)
        : base(message)
    {
    }

    public EmployeeListNotFoundException(string message, Exception inner)
        : base(message, inner)
    {
    }

// 如果异常需要跨应用程序域、跨进程或者跨计算机抛出,就需要能被序列化。
    protected InvalidDepartmentException(SerializationInfo info, StreamingContext context) : base(info, context) { }

}

microsoft如何创建用户定义的异常

使用远程处理时,必须确保所有用户定义的异常的元数据在服务器(被调用方)可用,在客户端(代理对象或调用方)也可用。 有关详细信息,请参阅异常的最佳做法。

指导原则和最佳实践

善用finally块该不该引发异常 小节中我们说到一个异常会被引发,是因为某个方法声称的任务没有成功完成(失败),而失败的原因有四种:

  1. 方法的使用者用错了(没有按照方法的契约使用)
  2. 方法的执行代码写错了
  3. 方法执行时所在的环境不符合预期

简单说来,就是:使用错误,实现错误、环境错误。

使用错误:

  • ArgumentException 表示参数使用错了
  • ArgumentNullException 表示参数不应该传入 null
  • ArgumentOutOfRangeException 表示参数中的序号超出了范围
  • InvalidEnumArgumentException 表示参数中的枚举值不正确
  • InvalidOperationException 表示当前状态下不允许进行此操作(也就是说存在着允许进行此操作的另一种状态)
  • ObjectDisposedException 表示对象已经 Dispose 过了,不能再使用了
  • NotSupportedException 表示不支持进行此操作(这是在说不要再试图对这种类型的对象调用此方法了,不支持)
  • PlatformNotSupportedException 表示在此平台下不支持(如果程序跨平台的话)
  • NotImplementedException 表示此功能尚在开发中,暂时请勿使用

实现错误:

前面由 CLR 抛出的异常代码主要都是实现错误

  • NullReferenceException 试图在空引用上执行某些方法,除了告诉实现者出现了意料之外的 null 之外,没有什么其它价值了
  • IndexOutOfRangeException 使用索引的时候超出了边界
  • InvalidCastException 表示试图对某个类型进行强转但类型不匹配
  • StackOverflowException 表示栈溢出,这通常说明实现代码的时候写了不正确的显式或隐式的递归
  • OutOfMemoryException 表示托管堆中已无法分出期望的内存空间,或程序已经没有更多内存可用了
  • AccessViolationException 这说明使用非托管内存时发生了错误
  • BadImageFormatException 这说明了加载的 dll 并不是期望中的托管 dll
  • TypeLoadException 表示类型初始化的时候发生了错误

环境错误:

  • IOException 下的各种子类
  • Win32Exception 下的各种子类
  • ……

另外,还剩下一些不应该抛出的异常,例如过于抽象的异常和已经过时的异常,这在前面一小结中有说明。

C