撰写了文章 发布于 2020-04-22 17:14:56
从C#开始的编程入门——异常处理
无论是多年经验的程序员,还是只会写Hello, world的人,程序运行过程中,发生异常是很常见的事情(你要相信写hello world也是可能会发生异常的)。一方面可能你的程序本身就有问题,但是到了运行时才会发生异常。另一方面有一些外部因素,造成了本该正常进行的程序发生了异常,并且你的程序无法处理这个异常。程序员在编写代码过程中需要考虑到异常发生的可能性,对异常进行处理,判断后序流程,或者在必要时抛出异常,中断程序的执行。
比如说:
int[] array = new int[]{1,2,3,4,5};
System.Console.WriteLine(array[6]);
编译时不会发生如何异常,因为编译器不会假定array在被访问时没有第七个元素。但是只要你运行这个程序,就必然会抛出一个异常。具体到这个例子来说,会抛出一个System.IndexOutOfRangeException类型的异常,提示信息为‘Index was outside the bounds of the array.’,即索引超过了数组的边界。由于没有处理这个异常的发生,最终程序也就中止了。
异常总是会发生,但是并非所有的异常我们都无法处理而只能终止程序的运行,而且还有些时候面对我们的程序无法处理的情况,我们可能需要主动抛出异常终止程序。这就需要一种更好的方法来处理异常。
try-catch-finally
和很多C系语言(实际上一些不是C系语法的语言也采用try等关键字用于异常处理)一样,C#的一个典型的异常处理代码由几个不同的部分组成。try代码块表示其中的代码可能会引发异常,catch代码块表示在前面的try中发生某种异常时要执行的操作。最后还有一个可选的finally,无论异常是否发生,最终都将会执行这里面的代码。比如说我们有一个简单获取数组内容的方法:
public static int GetAt(int[] array, int index)
{
return array[index];
}
我们这个方法最终的目的是要返回对应位置上值,而参数index可能会超出数组边界。我们可以用if去判断index情况,但是我们不可能去返回一个随意的值来代表索引不合法(当然我们也有其它的处理方式,但我们这里主要是演示错误处理)。就像前面提到的,这里可能会抛出索引越界的异常。这个时候我们可以像这样处理外部调用这个方法的代码:
int[] array = new int[] { 1, 2, 3, 4, 5 };
try
{
var n = GetAt(array, 6);
}
catch (IndexOutOfRangeException)
{
System.Console.WriteLine(“Index out of range.”);
}
当然这里我们可以预料地会发生越界异常也就是InIndexOutOfRangeException,当try中的代码发生异常,catch就会捕捉到这个异常,catch关键字后面有一个括号,有点像一个参数列表,但是这里面只能放进各种异常类型的东西,也就是从System.Exception类型派生出的各种异常类型。catch的意思就是,一旦try中发生了这个类型的异常,就执行后面的代码。这里我们简单输出索引越界的信息。
Exception类及其派生类实际上包含了发生异常时的相关信息,如果在catch之后我们需要用到一些异常相关的信息来处理,我们可以声明一个变量来代表这个异常,就像一个参数一样。比如说我们把上面的catch改为:
catch (IndexOutOfRangeException e)
{
System.Console.WriteLine(e.Message);
}
习惯上使用e(exception)来命名一个异常,然后我们就可以在catch中访问异常的相关信息。Message属性是Exception类中定义的一个异常相关信息的属性,它是string类型的,一句简单描述异常的信息。
捕获多种类型的异常
现在我们对上面的代码稍作修改。
int[] array = new int[] { 0, 1, 2, 3, 4, 5 };
try
{
var n = GetAt(array, 0);
System.Console.WriteLine( 1 / n );
}
// ……
现在数组第一个元素为0,意味着GetAt方法有可能会拿到一个0,随后的除法运算可能会发生0作除数的情况,这是说不通的情况,只能抛出异常。这里我明确地告诉你,0作除数的时候会发生DivideByZeroException异常,名字再明显不过了。不过,这个异常和我们前面写的catch中的异常类型不同,而catch只会捕捉指定类型的异常(或者括号中异常类型派生出来的异常),这两个类型并不存在**关系。并且我们不能确定程序运行时究竟会发生哪一个异常。这个时候我们需要多写一个catch就行了。
// ……
catch (IndexOutOfRangeException e)
{
System.Console.WriteLine(e.Message);
}
catch (DivideByZeroException)
{
System.Console.WriteLine(“Divided by zero”);
}
当一个异常发生在try中,会尝试拿这个异常从上至下逐个对比所有catch中的异常类型,当一个catch的异常类型和发生的异常类型一致,或者是发生的异常的基类时,就会选择这个catch执行。但是即使有多个catch可以接受,最终也只会选择最早匹配的那个执行。因此应当将更具体的异常类型放在较前面以确保能够得到正确的处理。并且不建议使用所有异常的基类Exception,除非你真的知道如何处理try中发生的任何类型的异常。
调用栈(callstack):程序执行的过程常常是不断调用函数(方法)的过程。函数调用的先后顺序就形成了一个栈。在调试程序的过程中,异常发生时检查调用栈是非常常见的调试方法之一。它能够表明是在哪里、怎样调用这个方法时发生了异常。C#的异常类型中的StackTrace就代表着发生异常时调用栈的情况。而很多IDE的调试功能也支持查看调用栈。
最终,如果没有任何catch匹配,这个异常将被丢向调用这个方法的上一级方法(调用栈更下面的方法),然后执行同样的错误处理流程,如果最终都没有代码处理这个异常,那么程序将会直接像用户提示异常并终止。
收尾工作
在catch之后(如果有catch的话),还可以有finally块。finally不管异常是否发生,发生异常时是否有catch捕捉到,finally中的代码都会执行。实际情况中finally主要用于释放try中分配的资源等等。
// ……
finally
{
System.Console.WriteLine(“Program exit”);
}
主动抛出异常
前面的例子中,我们都是尝试用try和catch处理我们在调用其他人写的代码中可能抛出的异常。然而有些时候在我们自己的代码中遇到一些不能处理的情况可能需要主动抛出异常以表明我们被调用的某个方法无法完成原本的任务,通过抛出异常给调用方(caller)让上层的代码处理这个异常。比如说,很多时候我们可以通过IDE的快速重构操作来自动生成一些方法,由于有些时候IDE不能帮你推断出如何实现方法,只是帮你生成一些大概的声明,比如说IDE帮你实现接口时通常在定义中只会留下这样一句代码:
throw new NotImplementedException();
throw关键字实际上就是表示我们需要主动地抛出一个异常。这里的异常是NotImplementedException,表示这个方法现在并没有被实现,因此一旦我们错误的调用的,这个还没有被实现的方法程序就会发生异常。
另外,向在前面一词中的访问数组的索引时,如果索引出现的越界的问题,我们也可以主动地抛出这样的异常。
另外,在try和catch中,我们也可以使用throw关键字。在try中遇到了主动抛出的异常那么要么被catch捕获,要么就向上层代码继续抛出。而在catch中,就算我们捕获了这个异常,我们也可以将这个异常继续向外抛出,把处理异常的权限交给外外部代码。
定义自己的异常
在标准苦中提供了很多自带的异常类型,当然我们也可以实现自己的异常类型,毕竟标准库中的异常类型可能并不能很好地描述我们自己的程序中会发生的异常。我们可以通过继承System.Exception类来定义自己的程序中会发生的异常,并给予相关的信息。
定义异常时应当至少定义一个四个构造器:一个无参构造器、一个设置Message属性的构造器、一个设置Message和InnerException(代表造成当前异常的另一个异常)属性的构造器、一个用于序列化异常的构造器。
关于异常的注意事项
那么什么时候才应该处理异常呢,这个问题一开始会显得很难回答,因为有时候我们可能会想到使用if等方法去判断条件似乎也能完成一些错误的处理。
微软的文档中对于捕获异常提到了以下几点:
- 你清楚可能会有什么异常发生,并且你能针对异常做出一些恢复工作
- 你可以创建并抛出一种更加具体的异常
- 在异常发生被传递到其他地方之前你需要做出一些额外处理
对于何时抛出异常:
- 方法无法完成定义的功能
- 在对象上调用了对于当前对象状态来说不合适的方法
- 方法的参数造成了异常
抛出异常时应当避免:
- 异常不应当被用作改变程序通常的执行流程。只能用于报告和处理错误状况
- 异常不应该被作为返回值
- 不要在你的代码中有意抛出 System.Exception(异常的基类), System.SystemException , System.NullReferenceException(空引用异常,通常发生在你意外尝试在null**问成员的时候), System.IndexOutOfRangeException
- 不要创造一些在调试时可以抛出但不会在发布时抛出的异常
一般来说,错误处理应该在错误来自外部时进行。比如说程序需要访问的服务器停机了连不上、打开文件的权限不够、磁盘容量不够、用户有输入了错误的信息等等。最关键的是由于这些问题导致了程序无法正常进行某项功能,并且错误的根源在于无法干涉的外部(比如由于网线断了你的程序连不上服务器你总不可能跳出电脑去把网线接上吧)。对于完全是你的程序内部造成的问题,你不应该抛出一个异常(当然如果你的项目本身就是一个类库,你是应当抛出一些异常来告知引用类库的程序),因为这样的问题是你在开发时应该尝试解决的问题,这样的问题即使在运行时发生了,也不会有人能够帮你解决(这属于你的程序的一个bug,你应该尽快解决)。
目录