try 语句是为了处理错误或者执行清理代码而定义的语句块。 try 语句块后面必须跟一个或多个 catch 语句块或 finally 语句块,或者两者都有。当 try 语句块执行发生错误时,就会执行 catch 语句块。当 try 语句块结束时(或者如果当前是 catch 语句块且当 catch 语句块结束时),不管有没有发生错误,都会执行 finally 语句块来执行清理代码。
catch 语句块可以访问 Exception 对象,该对象包含错误信息。我们可以在 catch 语句块中处理错误或者再次抛出异常。例如,记录日志并重新抛出异常,或者抛出一个更高层次的异常。
finally 语句块为程序的执行提供了确定性,CLR会尽最大努力保证其执行。它通常用于执行清理任务,例如,关闭网络连接等。
try 语句的使用示例如下:
考虑如下程序:
由于 x 是 0 ,因此运行时将抛出 DivideByZeroException ,程序终止。我们可以通过 catch 捕获异常来防止程序提前终止:
输出为:
上述程序是异常处理的简单示例。在实际工作中,更好的方法是在调用 Calc 之前显式检查除数是否为 0 。
我们更提倡提前进行检查以避免错误,而不是依赖 try / catch 语句块。这是因为异常处理代价比较大,通常需要超过几百个时钟周期。
当 try 语句中抛出异常时,公共语言运行时(CLR)会执行如下测试:
try 语句是否具有兼容的 catch 语句块?
· 如果有,则执行点转移到可以处理相应异常的 catch 语句块,之后再跳转到 finally 语句块(如果有的话),再继续正常执行。
· 如果没有,则执行会直接跳转到 finally 语句块(如果有的话),之后CLR会从调用栈中寻找其他 try 语句块,若找到则重复上述测试。
如果没有任何函数处理该异常,则程序将终止执行。
catch 子句定义捕获哪些类型的异常,这些异常应当是 System.Exception 或者 System.Exception 的子类。
捕获 System.Exception 表示捕获所有可能的异常,通常用于如下场景:
· 不论何种特定类型的异常,程序都可以恢复。
· (在记录日志之后)重新抛出该异常。
· 程序终止前的最后一个错误处理函数。
比上述更常见的做法则是捕获特定类型的异常(例如 OutOfMemoryException ),以避免出现设计中遗漏特定情景的情况。
可以使用多个 catch 子句处理多种异常类型(同样,以下例子也可以进行显式参数检查而不仅仅是进行异常处理):
一个 catch 子句只针对一种给定的异常。如果想通过捕获更普遍的异常(如 System.Exception )来构建安全网,则必须把处理特定异常的逻辑放在前面。
如果不需要访问异常的属性,则可以捕获异常但不指定变量:
甚至,可以同时忽略异常的类型和变量(捕获所有的异常):
我们可以在 catch 子句中添加 when 子句来指定异常筛选器(exception filter):
如果本例中抛出了 WebException ,则 when 关键字后指定的布尔表达式就会执行。如果执行结果为 false ,则 catch 语句块会被忽略,继而评估后续的 catch 语句块。有了异常筛选器之后,我们就可以重复捕获同类型的异常了:
when 子句中的布尔表达式可以包含副作用,例如,调用一个方法来记录诊断所需的异常。
无论代码是否抛出异常,也无论 try 语句块是否完全执行, finally 语句块总会执行。通常, finally 语句块用于执行清理工作。
finally 语句块会在以下任一种情况后执行:
· 在 catch 语句块执行完成后(或抛出一个新的异常时)。
· try 语句块执行完成后(或者抛出了一个异常但没有任何 catch 语句块针对该异常)。
· 控制逻辑使用 jump 语句(例如, return 或 goto )离开了 try 语句块。
唯一能够阻止 finally 语句块执行的就只有无限循环,或者应用程序进程突然终止了。
finally 语句块为程序添加了确定性保证。在以下例子中,即使发生了列表中的情况,打开的文件也总是能够关闭:
· try 语句块正常结束。
· 因为是空文件( EndOfStream )而提前返回了。
· 读取文件时抛出了 IOException 。
在本例中,我们通过 StreamReader 的 Dispose 方法来关闭文件。在 finally 语句块中调用 Dispose 方法是一种标准约定,在C#中也有 using 语句对此提供直接支持。
许多类的内部都封装了非托管资源,例如,文件句柄、图像句柄、数据库连接等。这些类都实现了 System.IDisposable 接口,该接口定义了一个名为 Dispose 的无参数方法,用于清除这些非托管资源。 using 语句提供了一种优雅方式,可以在 finally 块中调用 IDisposable 接口对象的 Dispose 方法。
因而以下语句:
完全等价于:
如果我们忽略 using 语句后的括号和语句块,那么 using 语句就成了 using 声明(C# 8)。相应的资源会在程序执行到该声明所在语句块外时释放:
在上述代码中,当程序执行到 if 语句块外时将调用 reader 对象的 Dispose 方法。
代码在运行时或在用户代码中都可以抛出异常。以下例子中, Display 方法会抛出: System.ArgumentNullException :
检查参数是否为 null 并适时抛出 ArgumentNullException 异常是再平常不过的操作了,因此在.NET 6中我们可以将其简写为:
请注意,我们无须指定参数的名称,这将在4.15.1节中给出解释。
throw 也可以以表达式的形式出现在表达式体函数中:
throw 表达式也可以出现在三元条件表达式中:
异常被捕获后可以再次抛出,例如:
如果将 throw 替换为 throw ex ,那么这个例子仍然有效。但是新产生异常的 StackTrace 属性不再反映原始的错误。
重新抛出异常可用于需要记录错误但是并不将异常隐藏的情形,也可以在异常超出处理范围的情况下放弃对异常进行处理。另一种常见情形是重新抛出某个类型更加具体的异常:
请注意,当构建 XmlException 时,我们将原始的异常 ex 作为第二个参数。这个参数将作为新异常的 InnerException 属性而辅助诊断。几乎所有类型的异常都提供了类似的构造器。
在跨越信任边界时,常见做法是重新抛出一个不那么明确的异常,以防止因技术信息泄露而给黑客可乘之机。
System.Exception 类有下面几个重要属性:
StackTrace
表示一个异常从起源到 catch 语句块的所有调用方法的字符串。
Message
描述异常的字符串。
InnerException
导致外部异常的内部异常(如果有的话)。而内部异常本身也可以有另外一个 InnerException 。
所有的C#异常都是运行时异常,没有和Java对等的编译时checked异常(checked exception)。
以下所列的异常类型在CLR和.NET库中广泛使用,可以在程序中抛出这些异常或者将其作为基类型来派生自定义的异常类型:
System.ArgumentException
当使用不恰当的参数调用函数时抛出该异常。这通常表明应用程序有缺陷。
System.ArgumentNullException
ArgumentException 的子类。当函数的参数(意外)为 null 时抛出该异常。
System.ArgumentOutOfRangeException
ArgumentException 的子类。当(通常是数字)参数太大或者太小时抛出该异常。例如,当向只能接受正数的函数传递负数时会抛出该异常。
System.InvalidOperationException
无论参数值如何,当对象的状态无法令方法成功执行时抛出该异常。例如,读取未打开的文件或在列表对象已修改的情况下用枚举器访问下一个元素时会抛出该异常。
System.NotSupportedException
当不支持特定的功能时抛出该异常。例如,当在一个 IsReadOnly 为 true 的集合上调用 Add 方法时会抛出该异常。
System.NotImplementedException
当特定的函数还没有实现时抛出该异常。
System.ObjectDisposedException
当函数调用的对象已被销毁时抛出该异常。
另一个常见的异常类型是 NullReferenceException 。当访问null对象的成员时,CLR就会抛出这个异常(表示代码有缺陷)。使用下面的语句会直接抛出一个 NullRe-ferenceException 异常(仅用于测试目的):
当编写方法时需要考虑方法出错时的行为,可以返回某个特定的错误代码,或抛出一个异常。一般情况下,如果错误发生在正常的工作流程之外,或者方法的直接调用者很可能无法处理这个错误时选择抛出异常。但是有些情况下最好给调用者提供两种选择。 int 类型是一个典型的例子,它为 Parse 方法定义了两个版本:
如果解析失败,则 Parse 方法抛出一个异常,而 TryParse 方法则返回 false 。
可以用如下方式令 XXX 方法调用 Try XXX 方法来实现这种模式:
在 int.TryParse 中,函数可以通过返回类型或者参数向调用函数返回错误代码。尽管这种方式对于简单的可预见性错误可行,但是针对所有的错误类型就显得捉襟见肘了。这样做不仅会使方法的签名晦涩,而且增加了不必要的复杂性,使代码变得混乱。此外,这种做法也不能推广到运算符(例如,除法运算符)、属性等不是方法的函数上。此时一种替代方式是将错误放在一个公共的地方,使其对调用栈中的所有函数都可见(例如,每一个线程中存储当前错误的静态方法)。但是,这要求每一个函数都参与到这种错误传播模式中,因此这种方式既冗长又容易出错。