标签:RoCE 返回 影响 message 用户自定义异常 性能 自动 基本 转换方法
大部分的错误(Error)通常不是写代码的人引发的。有的时候仅仅只是因为一些用户的误操作,又或者是程序的环境上下文运行出错,程序会抛出一个错误。在任何情况下,你都需要提前预估那些会发生在你的程序代码里的错误,做好后续处理。
.NET加强了你处理错误的能力。C#的错误处理机制使你可以提供对任何类型错误的自定义处理,这样代码就被分成两部分,一部分用来识别错误,一部分用来处理错误。
不管你的代码水平有多高,你的程序也必须能够处理任何可能发生的错误。举个例子,在处理一些特别复杂的代码的时候,你可能会发现你没有读取本地文件的权限;又或者,当你的程序发送一个网络请求时,网络瘫痪了。在这些异常情况下,程序仅仅返回一个错误代码是不够的 —— 一段复杂代码可能会有十几二十个嵌套方法调用。事实上你更愿意你的代码能跳过这些出错的内容,完成整个任务,即使执行得不那么完美。C#提供了一种非常好的异常处理机制,可以帮你解决这个情况。
本章涵盖了在不同场景下如何捕获与抛出异常的各种内容。你将看到不同命名空间下的各种异常类型,以及他们的层次结构。然后你会明白如何创建一个自定义的异常类。你将学会使用不同的方式来捕获异常——例如,如何获取基础的异常类型或者它的某个特例。你将会看到如何使用嵌套的try代码块,以及这些代码又是如何捕获异常的。而对于那些不管有没有发生异常都需要执行的代码,我们将会为你介绍try/finally代码块的使用。
通过本章你将会更好的掌握各种异常处理的高级技巧。
在C#里,当某块代码出现异常的时候,其实是创建(或者说抛出了)了一个异常对象。这个对象包含了各种相关信息以便你能识别错误出现的地方。虽然你也可以创建你自己的异常处理类(后面会介绍),.NET提供了许多预先定义好的异常类——不是一般的多,涵盖了各种可能的错误。下面的类层级图仅仅只是展示了其中的冰山一角,好让你对系统异常的组织模式(gerneral pattern)有个基础的认识。本小节会让你快速的了解.NET基础类库提供的一小部分异常。
上图中的所有类,基本上都是System命名空间下的一员,只有IOException、CompositionException以及这俩的子类不是。IOException的命名空间是System.IO。这个命名空间主要处理的是文件的读写。而CompositionException和它的子类则是System.ComponentModel.Composition的一部分,这个命名空间主要处理动态加载部分和组件管理。一般来说,异常没有别的命名空间了。异常类需要放在任何可以处理它们的类的命名空间下——因此,I/O相关的异常就在System.IO命名空间下。在基础类命名空间下你可以找到相当多的异常类。
通用的异常处理类,System.Exception,继承自System.Object,正如你所想,是一个.NET类。一般来说,你不能直接在你的代码里抛出通用的System.Exception对象,因为它们无法定位具体的错误信息。
在上面的层次结构中,有两个非常重要的类,继承自System.Exception:
上图中另外的一些异常类也非常有用:
上图中还有一些异常类我们不在这里做进一步的讨论,它们仅仅只是为了拿来演示异常类的层次结构。
异常类虽然有着自己的层次结构,然而大部分的子类,并不会为它们各自的基类添加任何新的功能,这在类的继承里不常见。然而,在异常类的处理中,添加一个继承子类,通常是为了描述更具体的特定错误情况。它没有覆写父类方法或者添加一些新方法的必要(但它们常常会添加一些额外的Property以便能更详细的描述错误情况)。举个例子,你可能会在方法的调用过程中使用ArgumentException来处理参数传递的异常,而一个更具体的异常ArgumentNullException可以派生于它,专门用来处理传递的参数中带有null值的情况。
上面我们介绍了.NET内置了很多预定义的基础异常类型,本节主要讲述的是,你如何在代码里捕获(trap)不同的错误情况。为了处理C#代码里可能会出现的异常,你往往会将你的代码分为三个不同的代码块:
下面的步骤概述了这三个代码块是如何协同工作的:
这三部分的C#语法是这样子的:
try
{
// code for normal execution
}
catch
{
// error handling
}
finally
{
// clean up
}
这个方式可以有些变化:
到目前为止还好,但仍然有一个问题没有被解答:当代码在try语句块中正常执行的时候,它是如何知道出错了,得切换到catch块里进行处理的?假如程序检测到一个错误,代码内部就会执行一些抛出异常的操作。换句话说,它会实例化一个异常类的对象,并且像这样抛出它:
throw new OverflowException();
这里,我们实例化了一个OverflowException的实例。只要程序在try语句块中遇到throw语句,它会立马检查与该try块关联的catch块。如果有多个catch块,它通过判断catch块关联的异常类,来决定应该进入哪个catch块进行异常处理。例如,当OverflowException实例被抛出的时候,执行将会跳到一个像这样的catch块中:
catch (OverflowException ex)
{
// exception handling here
}
换句话说,程序会优先查找那些跟抛出的异常实例完全一致(或者与它的父类一致)的catch块。
基于这一点,你可以扩展上面示例的try语法块。假设由于参数的原因,try块里可能会产生两个严重错误:一个数据溢出(overflow)或者一个数组越界(array out of bounds)。我们假定你代码里有两个布尔变量,一个叫Overflow,另外一个叫OutOfBounds,分别用来定义是否存在错误。前面你已经见过了预定义的OverflowException,同样的,C#里也定义了数组越界的异常IndexOutOfRangeException。
扩展后的try代码块可能是这样子的:
try
{
// code for normal execution
if (Overflow == true)
{
throw new OverflowException();
}
// more processing
if (OutOfBounds == true)
{
throw new IndexOutOfRangeException();
}
// otherwise continue normal execution
}
catch (OverflowException ex)
{
// error handling for the overflow error condition
}
catch (IndexOutOfRangeException ex)
{
// error handling for the index out of range error condition
}
finally
{
// clean up
}
C#允许你在代码里主动使用throw语句抛出一些异常,就算你不写,当代码执行到同样的错误的时候,运行时也会帮你自动生成一样的异常。而如果没有错的话,try语句块就正常执行其他的方法调用。当应用程序遇到throw语句的时候,它会马上终止当前的方法调用,转到try语句块的结尾,开始查找是否有相应的catch语句块处理。在这个过程中,所有的方法调用里的内部变量都会因为超出作用域而无效。这种try...catch处理方式非常适合本章开头说的那种程序死循环的情况,当任何一个方法调用出错时,try语句块中的所有方法调用都会被立刻停止,不管里面写了15个还是20个方法。
你可能会从这个案例中总结出更多有意义的try语句块应用场景。但是,你要明白异常处理机制最主要的目的就是用来处理异常情况的,就如它们名字定义的一样。你千万不要试图用它们来控制代码的执行过程,譬如用异常控制何时退出一个do...while循环。
异常类的处理上会影响到性能。如果可以的话,你不要频繁地使用异常类来处理异常。举个例子,当你将一个string对象转换成number类型的时候,你可以使用int类型的Parse方法。当字符串无法转换成number的时候,Parse方法会抛出一个FormatException,而如果它转换的值超出了int类型定义的数值范围的话,则会抛出一个OverflowException,你的代码可能是这样子:
try
{
int i = int.Parse(n);
Console.WriteLine($"converted: {i}");
}
catch (FormatException ex)
{
Console.WriteLine(ex.Message);
}
catch (OverflowException ex)
{
Console.WriteLine(ex.Message);
}
如果你只是普通的接收一个字符串值n然后正常地将它转换成int类型的数值,期间没有出现任何异常的话,你可以这么写。但是,假如你多次调用这种转换方式,而有不少情况会出现各种转换异常的话,这种写法就会有很大的性能损失。更建议的方式是使用TryParse方法,当字符串无法正常转换成数值时,这个方法不会抛出任何异常,当能转换时,该方法会返回true,不能则返回false,你可以像下面这样写:
static void NumberDemo2(string n)
{
if (n is null) throw new ArgumentNullException(nameof(n));
if (int.TryParse(n, out int result))
{
Console.WriteLine($"converted {result}");
}
else
{
Console.WriteLine("not a number");
}
}
想看try...catch..finally语句块实际是如何执行的,最简单的方式就是写几个例子。我们的第一个例子叫SimpleExceptions。它重复地获取用户输入的数字,然后在控制台上显示。为了代码的演示效果,我们假定只有0-5之间的数字是有效的,其他的数值这个程序都处理不了;因此当你输入一个超出范围的数值的时候,程序就会抛出一个异常。只有在接收到一个空的Enter之后,我们的程序才会结束运行。
注意:本例子只是为了演示异常是如何被捕捉和处理的,并不是一个好的异常使用示例,千万不要在业务环境里这么写。就像"异常"这个名字的含义一样,异常是用来处理那些意料之外的情况的,而非应付可预期的代码逻辑。用户经常会输入一些奇怪的内容,而且会有多奇怪多蠢完全无法预计。正常情况下,你的程序会验证一些无效的输入,提示用户检查或者重新输入正确的内容。而想通过一个小代码示例,让你在短短几分钟内理解异常情况具体是怎么处理的,并不容易。所以我才会设计了这么一个不太理想的示例来简单地演示异常是如何工作的。后续的示例会更加的贴合实际一些。这个例子的代码如下所示:
public class Program
{
public static void Main()
{
while (true)
{
try
{
string userInput;
Console.Write("Input a number between 0 and 5 " + "(or just hit return to exit)> ");
userInput = Console.ReadLine();
if (string.IsNullOrEmpty(userInput))
{
break;
}
int index = Convert.ToInt32(userInput);
if (index < 0 || index > 5)
{
throw new IndexOutOfRangeException($"You typed in {userInput}");
}
Console.WriteLine($"Your number was {index}");
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("Exception: " + $"Number should be between 0 and 5. {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"An exception was thrown. Message was: " + $"{ex.Message}");
}
finally
{
Console.WriteLine("Thank you\n");
}
}
}
}
这段代码的核心是一个while循环,它使用ReadLine方法持续读取控制台的用户输入。ReadLine方法返回一个string类型的值,代码的第一步就是使用System.Convert.Int32方法将它转换成一个int类型。System.Convert类提供了很多有用的数据转换方法,是int.Parse以外的可选方案。通常来讲,System.Convert类可以处理多种数据类型的变换。调用它可以让编译器生成一个基于System.Int32的int实例。
在上面的例子里,我们也检查了是否输入了一个空的字符串,因为我们把它当做退出while循环的必要条件。注意break语句实际上跳出的是while循环,因为try代码块只是一个异常处理的封装,而while循环才是有效的代码块。当然,在break语句跳出while循环前,因为它包含在try语句块内,因此关联的finally语句块也会被执行。虽然这里你可能只是显示了一句谢谢,然而更常见的方式是,你会在这个语句块里做一些譬如,关闭文件句柄,调用不同对象的Dispose方法,释放占用的资源之类的操作。当程序离开finally代码块时,程序会接着执行其他部分的代码。在上面的例子里,你会重新迭代回while循坏开头的位置,从try代码块接着执行(除非finally块是紧跟着break语句之后执行的,那样则是最后一次执行finally块了,执行完就直接退出了while循环)。
接下来,你检查了你的异常条件:
if (index < 0 || index > 5)
{
throw new IndexOutOfRangeException($"You typed in {userInput}");
}
当你需要抛出一个异常的时候,你需要指定抛出的是什么类型。虽然你也可以笼统地使用System.Exception,但它仅仅是个基类。如果你只是抛出一个基础类型的异常,其实是一种不好的程序实践,因为它无法传达错误信息产生的本质。与之相对的是,.NET包含很多派生于它的具体Exception类,每一个派生类都匹配一种特定的异常情况,而且.NET也允许你定义属于你自己的异常类。这种设计的目的是希望提供尽可能详尽的错误信息,以便快速定位错误来源。在上面的这个例子里,比起System.Exception,System.IndexOutOfRangeException是一个更好的选择。IndexOutOfRangeException拥有不同的构造函数,例子中使用的是带错误描述字符串的这种。另外,你也可以选择继承Exception,实现你自定义的异常类,以便提供你程序所需的上下文错误信息。
假如用户输入了一个0-5之外的数字,并且进入了上面的if块内,一个IndexOutOfRangeException对象实例就会被创建,并且抛出。此时,应用程序会马上结束try块的执行,并且catch块会捕获到这个IndexOutOfRangeException异常进行处理。第一个catch块会被调用:
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("Exception: " + $"Number should be between 0 and 5. {ex.Message}");
}
因为这个catch块里明确指定了异常类型的参数,因此在接收到同样类型的异常时,会优先进入这个catch块进行处理。这里面在控制台上显示了一个详细的异常信息ex.Message,取决于IndexOutOfRangeException的构造函数是怎么创建的。catch块执行完毕后,程序控制权切换给了finally块,就像没有任何异常发生过一样。
你可以注意到我们还提供了另外一个catch块:
catch (Exception ex)
{
Console.WriteLine($"An exception was thrown. Message was: " + $"{ex.Message}");
}
这个catch块当然也可以处理IndexOutOfRangeException,假如异常没有被前一个catch块捕获的话。一个基类的引用可以适配所有继承于它的子类异常,而我们所有的异常都派生于Exception,所以Exception可以捕获所有的异常。因为前面那个catch块的已经匹配上了,所以第二个catch块没有被执行。程序只会按顺序执行catch块,找到第一个适配的catch块,进入执行,后续的catch块则不会再次进行匹配。
假如用户输入了非数值类型——譬如输入了字母a或者单词hello——那么Convert.ToInt32方法则会抛出一个System.FormatException异常,来指明要转换的字符串不是一个有效的int类型的数字。当这个异常产生的时候,应用程序会回溯方法调用栈,查找是否有一段处理程序可以用来处理这个异常。很显然第一个catch块匹配的是IndexOutOfRangeException,跟这个不同,所以它不会被执行。而第二个catch块定义的是Exception类型,FormatException是Exception的子类,所以一个FormatException的实例可以被当做Exception参数传递进这个catch块。
上面的例子其实是一个相当典型的多catch块应用。开始的时候,你写上了各种catch块并且试图捕获不同类型的错误。最后,你写上了通用的catch块以便捕获那些你没有指定的错误类型。事实上,catch块的书写顺序是很重要的。如果你调整了上面例子的两个catch块的顺序,代码将会提示一个编译错误:"上一个 catch 子句已经捕获了此类型或超类型(Exception)的所有异常",所以第二个catch块永远无法生效。因此,最上方的catch块必须是最小粒度的特定异常,而最后的catch块则可以匹配更多的通用异常。
现在你已经明白了整段代码的内容,你可以尝试运行它。输入不同的内容,然后看看程序是如何显示的IndexOutOfRangeException和FormatException,你可能会看到这样的结果:
Input a number between 0 and 5 (or just hit return to exit)>4
Your number was 4
Thank you
Input a number between 0 and 5 (or just hit return to exit)>0
Your number was 0
Thank you
Input a number between 0 and 5 (or just hit return to exit)>10
Exception: Number should be between 0 and 5. You typed in 10
Thank you
Input a number between 0 and 5 (or just hit return to exit)>hello
An exception was thrown. Message was: Input string was not in a correct format.
Thank you
Input a number between 0 and 5 (or just hit return to exit)>
Thank you
标签:RoCE 返回 影响 message 用户自定义异常 性能 自动 基本 转换方法
原文地址:https://www.cnblogs.com/zenronphy/p/12495714.html