Pro C#10 CHAPTER 7 Understanding Structured Exception Handling

CHAPTER 7 Understanding Structured Exception Handling

第7章 了解结构化异常处理

In this chapter, you will learn how to handle runtime anomalies in your C# code through the use of structured exception handling. Not only will you examine the C# keywords that allow you to handle such matters (try, catch, throw, finally, when), but you will also come to understand the distinction between application-level and system-level exceptions, as well as the role of the System.Exception base class.
在本章中,您将学习如何通过使用结构化异常处理来处理 C# 代码中的运行时异常。您不仅将检查允许您处理此类问题的 C# 关键字(尝试、捕获、抛出、最后、何时),而且还将了解应用程序级和系统级异常之间的区别,以及 System.Exception 基类的作用。

This discussion will lead into the topic of building custom exceptions and, finally, to a quick look at some exception-centric debugging tools of Visual Studio.
此讨论将引出生成自定义异常的主题,最后快速浏览 Visual Studio 的一些以异常为中心的调试工具。

Ode to Errors, Bugs, and Exceptions

错误、错误和异常

Despite what our (sometimes inflated) egos may tell us, no programmer is perfect. Writing software is a complex undertaking, and given this complexity, it is quite common for even the best software to ship with various problems. Sometimes the problem is caused by bad code (such as overflowing the bounds of an array). Other times, a problem is caused by bogus user input that has not been accounted for in the application’s code base (e.g., a phone number input field assigned to the value Chucky). Now, regardless of the cause of the problem, the end result is that the application does not work as expected. To help frame the upcoming discussion of structured exception handling, allow me to provide definitions for three commonly used anomaly-centric terms.
尽管我们的(有时是膨胀的)自我可能会告诉我们,但没有一个程序员是完美的。编写软件是一项复杂的工作,鉴于这种复杂性,即使是最好的软件也会遇到各种问题。有时问题是由错误代码引起的(例如溢出数组的边界)。其他时候,问题是由未在应用程序的代码库(例如,分配给值 Chucky 的电话号码输入字段)。现在,无论问题的原因是什么,最终结果都是应用程序无法按预期工作。为了帮助构建即将进行的结构化异常处理讨论,请允许我提供三个常用的以异常为中心的术语的定义。

•Bugs: These are, simply put, errors made by the programmer. For example, suppose you are programming with unmanaged C++. If you fail to delete dynamically allocated memory, resulting in a memory leak, you have a bug.
错误:简单地说,这些是程序员犯的错误。例如,假设您正在使用非托管C++进行编程。如果无法删除动态分配的内存,从而导致内存泄漏,则存在错误。
•User errors: User errors, on the other hand, are typically caused by the individual running your application, rather than by those who created it. For example, an end user who enters a malformed string into a text box could very well generate an error if you fail to handle this faulty input in your code base.
用户错误:另一方面,用户错误通常是由运行应用程序的个人引起的,而不是由创建应用程序的人引起的。例如,如果您未能在代码库中处理此错误输入,则在文本框中输入格式错误的字符串的最终用户很可能会生成错误。
•Exceptions: Exceptions are typically regarded as runtime anomalies that are difficult, if not impossible, to account for while programming your application. Possible exceptions include attempting to connect to a database that no longer exists, opening a corrupted XML file, or trying to contact a machine that is currently offline. In each of these cases, the programmer (or end user) has little control over these “exceptional” circumstances.
异常:异常通常被视为运行时异常,在对应用程序进行编程时很难(如果不是不可能的话)考虑这些异常。可能的异常包括尝试连接到不再存在的数据库、打开损坏的 XML 文件或尝试联系当前处于脱机状态的计算机。在每种情况下,程序员(或最终用户)几乎无法控制这些“特殊”情况。

Given these definitions, it should be clear that .NET structured exception handling is a technique for dealing with runtime exceptions. However, even for the bugs and user errors that have escaped your view, the runtime will often generate a corresponding exception that identifies the problem at hand. By way of a few examples, the .NET base class libraries define numerous exceptions, such as FormatException, IndexOutOfRangeException, FileNotFoundException, ArgumentOutOfRangeException, and so forth.
鉴于这些定义,应该清楚的是,.NET 结构化异常处理是一种处理运行时异常的技术。但是,即使对于已逃脱视图的 bug 和用户错误,运行时通常也会生成相应的异常来标识手头的问题。顺便说一下在几个示例中,.NET 基类库定义了大量异常,例如 FormatException、IndexOutOfRangeException、FileNotFoundException、ArgumentOutOfRangeException 等。

Within the .NET nomenclature, an exception accounts for bugs, bogus user input, and runtime errors, even though programmers may view each of these as a distinct issue. However, before I get too far ahead of myself, let’s formalize the role of structured exception handling and check out how it differs from traditional error-handling techniques.
在 .NET 命名法中,异常会考虑错误、虚假用户输入和运行时错误,即使程序员可能将其中每个错误视为一个单独的问题。但是,在我走得太远之前,让我们正式定义结构化异常处理的角色,并检查它与传统的错误处理技术有何不同。

■ Note To make the code examples used in this book as clean as possible, I will not catch every possible exception that may be thrown by a given method in the base class libraries. In your production-level projects, you should, of course, make liberal use of the techniques presented in this chapter.
注意 为了使本书中使用的代码示例尽可能干净,我不会捕获基类库中给定方法可能引发的所有可能的异常。当然,在您的生产级项目中,您应该自由使用本章中介绍的技术。

The Role of .NET Exception Handling

.NET 异常处理的作用

Prior to .NET, error handling under the Windows operating system was a confused mishmash of techniques. Many programmers rolled their own error-handling logic within the context of a given application. For example, a development team could define a set of numerical constants that represented known error conditions and make use of them as method return values. By way of an example, consider the following partial C code:
在.NET之前,Windows操作系统下的错误处理是技术混乱的大杂烩。许多程序员在给定应用程序的上下文中引入了自己的错误处理逻辑。例如,开发团队可以定义一组表示已知错误条件的数值常量,并将其用作方法返回值。作为示例,请考虑以下部分 C 代码:

/ A very C-style error trapping mechanism. / #define E_FILENOTFOUND 1000

int UseFileSystem()
{
// Assume something happens in this function
// that causes the following return value. return E_FILENOTFOUND;
}

void main()
{
int retVal = UseFileSystem(); if(retVal == E_FILENOTFOUND)
printf("Cannot find file…");
}

This approach is less than ideal, given that the constant EFILENOTFOUND is little more than a numerical value and is far from being a helpful agent regarding how to deal with the problem. Ideally, you would like to wrap the error’s name, a descriptive message, and other helpful information about this error condition into a single, well-defined package (which is exactly what happens under structured exception handling). In addition to a developer’s ad hoc techniques, the Windows API defines hundreds of error codes that come by way of #defines, HRESULTs, and far too many variations on the simple Boolean (bool, BOOL, VARIANT BOOL, etc.).
这种方法不太理想,因为常数EFILENOTFOUND只不过是一个数值,远不能成为如何处理问题的有用代理。理想情况下,您希望将错误的名称、描述性消息和有关此错误条件的其他有用信息包装到单个定义明确的包中(这正是结构化异常处理下发生的情况)。除了开发人员的临时技术之外,Windows API 还定义了数百个错误代码,这些代码来自 #defines、HRESULTs 以及简单布尔值(bool、BOOL、VARIANT BOOL 等)的太多变体。

The obvious problem with these older techniques is the tremendous lack of symmetry. Each approach is more or less tailored to a given technology, a given language, and perhaps even a given project. To
put an end to this madness, the .NET platform provides a standard technique to send and trap runtime errors: structured exception handling. The beauty of this approach is that developers now have a unified approach to error handling, which is common to all languages targeting the .NET platform. Therefore, the way in which a C# programmer handles errors is syntactically similar to that of a VB programmer, or a C++ programmer using C++/CLI.
这些旧技术的明显问题是严重缺乏对称性。每种方法都或多或少地针对给定的技术、给定的语言,甚至可能是给定的项目量身定制的。自为了结束这种疯狂,.NET 平台提供了一种发送和捕获运行时错误的标准技术:结构化异常处理。这种方法的优点在于,开发人员现在具有统一的错误处理方法,这对于面向 .NET 平台的所有语言都是通用的。因此,C# 程序员处理错误的方式在语法上类似于使用 C++/CLI 的 VB 程序员或C++程序员。

As an added bonus, the syntax used to throw and catch exceptions across assemblies and machine boundaries is identical. For example, if you use C# to build a ASP.NET Core RESTful service, you can throw a JSON fault to a remote caller, using the same keywords that allow you to throw an exception between methods in the same application.
作为额外的好处,用于跨程序集和计算机边界引发和捕获异常的语法是相同的。例如,如果使用 C# 生成 ASP.NET 核心 RESTful 服务,则可以使用允许在同一应用程序中的方法之间引发异常的相同关键字,将 JSON 错误抛给远程调用方。

Another bonus of .NET exceptions is that rather than receiving a cryptic numerical value, exceptions are objects that contain a human-readable description of the problem, as well as a detailed snapshot of the call stack that triggered the exception in the first place. Furthermore, you are able to give the end user help-link information that points the user to a URL that provides details about the error, as well as custom programmer-defined data.
作为额外的好处,用于跨程序集和计算机边界引发和捕获异常的语法是相同的。例如,如果使用 C# 生成 ASP.NET 核心 RESTful 服务,则可以使用允许在同一应用程序中的方法之间引发异常的相同关键字,将 JSON 错误抛给远程调用方。

The Building Blocks of .NET Exception Handling

.NET 异常处理的构建基块

Programming with structured exception handling involves the use of four interrelated entities.
使用结构化异常处理进行编程涉及使用四个相互关联的实体。

•A class type that represents the details of the exception
表示异常详细信息的类类型
•A member that throws an instance of the exception class to the caller under the correct circumstances
在正确情况下将异常类的实例抛给调用方的成员
•A block of code on the caller’s side that invokes the exception-prone member
调用方端调用容易发生异常的成员的代码块
•A block of code on the caller’s side that will process (or catch) the exception, should it occur
调用方端的代码块,如果发生异常,它将处理(或捕获)异常

The C# programming language offers five keywords (try, catch, throw, finally, and when) that allow you to throw and handle exceptions. The object that represents the problem at hand is a class extending System.Exception (or a descendent thereof). Given this fact, let’s check out the role of this exception-centric base class.
C# 编程语言提供了五个关键字(try、catch、throw 、finally 和 when),可用于引发和处理异常。表示手头问题的对象是一个扩展 System.Exception 的类(或其后代)。鉴于这一事实,让我们看看这个以异常为中心的基类的作用。

The System.Exception Base Class

System.Exception 基类

All exceptions ultimately derive from the System.Exception base class, which in turn derives from System. Object. Here is the main definition of this class (note that some of these members are virtual and may thus be overridden by derived classes):
所有异常最终都派生自 System.Exception 基类,而基类又派生自 System。对象。下面是此类的主要定义(请注意,其中一些成员是虚拟的,因此可能被派生类覆盖):

public class Exception : ISerializable
{
// Public constructors
public Exception(string message, Exception innerException); public Exception(string message);
public Exception();

// Methods
public virtual Exception GetBaseException();
public virtual void GetObjectData(SerializationInfo info, StreamingContext context);

// Properties
public virtual IDictionary Data { get; } public virtual string HelpLink { get; set; } public int HResult {get;set;}
public Exception InnerException { get; } public virtual string Message { get; } public virtual string Source { get; set; } public virtual string StackTrace { get; } public MethodBase TargetSite { get; }
}

As you can see, many of the properties defined by System.Exception are read-only in nature. This is because derived types will typically supply default values for each property. For example, the default message of the IndexOutOfRangeException type is “Index was outside the bounds of the array.”
如您所见,System.Exception 定义的许多属性本质上都是只读的。这是因为派生类型通常会为每个属性提供默认值。例如,IndexOutOfRangeException 类型的默认消息是“索引超出数组的边界”。

Table 7-1 describes the most important members of System.Exception.
表 7-1 描述了 System.Exception 最重要的成员。

Table 7-1. Core Members of the System.Exception Type
表 7-1. 系统的核心成员。异常类型

System.Exception Property Meaning in Life
Data This read-only property retrieves a collection of key-value pairs (represented by an object implementing IDictionary) that provide additional, programmer-defined information about the exception. By default, this collection is empty.
此只读属性检索键值对的集合(由实现 IDictionary 的对象表示),这些键值对提供有关异常的其他程序员定义的信息。默认情况下,此集合为空。
HelpLink This property gets or sets a URL to a help file or website describing the error in full detail.
此属性获取或设置指向帮助文件或网站的 URL,以完整详细描述错误。
InnerException This read-only property can be used to obtain information about the previous exceptions that caused the current exception to occur. The previous exceptions are recorded by passing them into the constructor of the most current exception.
此只读属性可用于获取有关导致当前异常发生的先前异常的信息。通过将以前的异常传递到最新异常的构造函数中来记录它们。
Message This read-only property returns the textual description of a given error. The error message itself is set as a constructor parameter.
此只读属性返回给定错误的文本说明。错误消息本身设置为构造函数参数。
Source This property gets or sets the name of the assembly, or the object, that threw the current exception.
此属性获取或设置引发当前异常的程序集或对象的名称。
StackTrace This read-only property contains a string that identifies the sequence of calls that triggered the exception. As you might guess, this property is useful during debugging or if you want to dump the error to an external error log.
此只读属性包含一个字符串,用于标识触发异常的调用序列。正如您可能猜到的那样,此属性在调试期间很有用,或者如果要将错误转储到外部错误日志中。
TargetSite This read-only property returns a MethodBase object, which describes numerous details about the method that threw the exception (invoking ToString() will identify the method by name).
此只读属性返回一个 MethodBase 对象,该对象描述了有关引发异常的方法的大量详细信息(调用 ToString() 将按名称标识该方法)。

The Simplest Possible Example

最简单的例子

To illustrate the usefulness of structured exception handling, you need to create a class that will throw an exception under the correct (or one might say exceptional) circumstances. Assume you have created a new C# Console Application project (named SimpleException) that defines two class types (Car and Radio)
为了说明结构化异常处理的有用性,您需要创建一个类,该类将在正确(或可以说是异常)情况下引发异常。假设您已经创建了一个新的 C# 控制台应用程序项目(名为 SimpleException),该项目定义了两种类类型(汽车和收音机)。)

associated by the “has-a” relationship. The Radio type defines a single method that turns the radio’s power on or off.
与“HAS-A”关系相关联。Radio 类型定义打开或关闭无线电电源的单个方法。

namespace SimpleException; class Radio
{
public void TurnOn(bool on)
{
Console.WriteLine(on ? "Jamming…" : "Quiet time…");
}
}

In addition to leveraging the Radio class via containment/delegation, the Car class (shown next) is defined in such a way that if the user accelerates a Car object beyond a predefined maximum speed (specified using a constant member variable named MaxSpeed), its engine explodes, rendering the Car unusable (captured by a private bool member variable named _carIsDead).
除了通过包含/委派利用 Radio 类之外,Car 类(如下所示)的定义方式是,如果用户将 Car 对象加速到超过预定义的最大速度(使用名为 MaxSpeed 的常量成员变量指定),其引擎将爆炸,使 Car 不可用(由名为 _carIsDead 的私有布尔成员变量捕获)。

Beyond these points, the Car type has a few properties to represent the current speed and a user- supplied “pet name,” as well as various constructors to set the state of a new Car object. Here is the complete definition (with code comments):
除了这些点之外,Car 类型还有一些属性来表示当前速度和用户提供的“宠物名称”,以及用于设置新 Car 对象状态的各种构造函数。以下是完整的定义(带有代码注释):

namespace SimpleException; class Car
{
// Constant for maximum speed. public const int MaxSpeed = 100;

// Car properties.
public int CurrentSpeed {get; set;} = 0; public string PetName {get; set;} = "";

// Is the car still operational? private bool _carIsDead;

// A car has-a radio.
private readonly Radio _theMusicBox = new Radio();

// Constructors. public Car() {}
public Car(string name, int speed)
{
CurrentSpeed = speed;
PetName = name;
}

public void CrankTunes(bool state)
{
// Delegate request to inner object.
_theMusicBox.TurnOn(state);
}

// See if Car has overheated.

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order…", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed > MaxSpeed)
{
Console.WriteLine("{0} has overheated!", PetName); CurrentSpeed = 0;
_carIsDead = true;
}
else
{
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
}
}

Next, update your Program.cs file to force a Car object to exceed the predefined maximum speed (set to 100, in the Car class), as shown here:
接下来,更新 Program.cs 文件以强制 Car 对象超过预定义的最大速度(在 Car 类中设置为 100),如下所示:

using System.Collections; using SimpleException;

Console.WriteLine(" Simple Exception Example "); Console.WriteLine("=> Creating a car and stepping on it!"); Car myCar = new Car("Zippy", 20);
myCar.CrankTunes(true);

for (int i = 0; i < 10; i++)
{
myCar.Accelerate(10);
}
Console.ReadLine();

When executing the code, you would see the following output:
执行代码时,您将看到以下输出:

Simple Exception Example
=> Creating a car and stepping on it! Jamming…
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60

=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100 Zippy has overheated! Zippy is out of order…

Throwing a General Exception

引发常规异常

Now that you have a functional Car class, I’ll demonstrate the simplest way to throw an exception. The current implementation of Accelerate() simply displays an error message if the caller attempts to speed up the Car beyond its upper limit.
现在你有一个函数式 Car 类,我将演示引发异常的最简单方法。Accelerate() 的当前实现只是在调用方尝试将 Car 加速到超出其上限时显示错误消息。

To retrofit this method to throw an exception if the user attempts to speed up the automobile after it has met its maker, you want to create and configure a new instance of the System.Exception class, setting the value of the read-only Message property via the class constructor. When you want to send the exception object back to the caller, use the C# throw() statement. Here is the relevant code update to the
Accelerate() method:
若要改造此方法,以便在用户遇到其制造商后尝试加速汽车时引发异常,您需要创建和配置 System.Exception 类的新实例,通过类构造函数设置只读 Message 属性的值。当您要发送异常对象回到调用方,使用 C# throw() 语句。以下是对加速() 方法:

// This time, throw an exception if the user speeds up beyond MaxSpeed. public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order…", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
CurrentSpeed = 0;
_carIsDead = true;

// Use the "throw" keyword to raise an exception. throw new Exception($"{PetName} has overheated!");
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

Before examining how a caller would catch this exception, let’s look at a few points of interest. First, when you are throwing an exception, it is always up to you to decide exactly what constitutes the error in question and when an exception should be thrown. Here, you are making the assumption that if the program attempts to increase the speed of a Car object beyond the maximum, a System.Exception object should be thrown to indicate the Accelerate() method cannot continue (which may or may not be a valid assumption; this will be a judgment call on your part based on the application you are creating).
若要改造此方法,以便在用户遇到其制造商后尝试加速汽车时引发异常,您需要创建和配置 System.Exception 类的新实例,通过类构造函数设置只读 Message 属性的值。当您要发送异常对象回到调用方,使用 C# throw() 语句。以下是对加速() 方法:

Alternatively, you could implement Accelerate() to recover automatically without needing to throw an exception in the first place. By and large, exceptions should be thrown only when a more terminal condition has been met (e.g., not finding a necessary file, failing to connect to a database, and the like) and not used as a logic flow mechanism. Deciding exactly what justifies throwing an exception is a design issue you must always contend with. For the current purposes, assume that asking a doomed automobile to increase its speed is cause to throw an exception.
或者,您可以实现 Accelerate() 自动恢复,而无需首先抛出异常。总的来说,只有当满足更终端的条件(例如,找不到必要的文件、无法连接到数据库等)并且不使用时,才应该抛出异常。作为一种逻辑流机制。确定引发异常的确切理由是您必须始终应对的设计问题。出于当前目的,假设要求注定要失败的汽车提高速度会导致引发异常。

Second, notice how the final else was removed from the method. When an exception is thrown (either by the framework or by manually using a throw() statement), control is returned to the calling method (or by the catch block in a try catch). This eliminates the need for the final else. Whether you leave it in for readability is up to you and your coding standards.
其次,请注意最终的 else 是如何从方法中删除的。当引发异常时(由框架或手动使用 throw() 语句),控制权将返回到调用方法(或由 try catch 中的 catch 块)。这消除了对最终其他的需要。是否将其保留为可读性取决于您和您的编码标准。

In any case, if you were to rerun the application at this point using the previous logic in the top-level statements, the exception would eventually be thrown. As shown in the following output, the result of not handling this error is less than ideal, given you receive a verbose error dump followed by the program’s termination (with your specific file path and line numbers):
无论如何,如果此时要使用顶级语句中的先前逻辑重新运行应用程序,则最终将引发异常。如以下输出所示,不处理此错误的结果不太理想,因为您会收到一个详细的错误转储,然后是程序的终止(使用您的特定文件路径和行号):

Simple Exception Example
=> Creating a car and stepping on it! Jamming…
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100

Unhandled exception. System.Exception: Zippy has overheated!
at SimpleException.Car.Accelerate(Int32 delta) in [path to file]\Car.cs:line 52
at SimpleException.Program.Main(String[] args) in [path to file]\Program.cs:line 16

Catching Exceptions

捕获异常

■ Note For those coming to C# from a Java background, understand that type members are not prototyped with the set of exceptions they may throw (in other words, .neT Core does not support checked exceptions). For better or for worse, you are not required to handle every exception thrown from a given member.
注意 对于那些从 Java 背景来到 C# 的人,请了解类型成员不是使用它们可能引发的异常集进行原型设计的(换句话说,.neT Core 不支持检查异常)。无论好坏,您都不需要处理从给定成员引发的每个异常。

Because the Accelerate() method now throws an exception, the caller needs to be ready to handle the exception, should it occur. When you are invoking a method that may throw an exception, you make use of a try/catch block. After you have caught the exception object, you are able to invoke the members of the exception object to extract the details of the problem.
由于 Accelerate() 方法现在抛出异常,因此调用方需要准备好在异常发生时处理异常。当您调用可能引发异常的方法时,您可以使用 try/catch 块。捕获异常对象后,可以调用异常对象的成员来提取问题的详细信息。

What you do with this data is largely up to you. You might want to log this information to a report file, write the data to the event log, email a system administrator, or display the problem to the end user. Here, you will simply dump the contents to the console window:
如何处理这些数据在很大程度上取决于您。您可能希望将此信息记录到报告文件、将数据写入事件日志、向系统管理员发送电子邮件或向最终用户显示问题。在这里,您只需将内容转储到控制台窗口:

// Speed up past the car’s max speed to
// trigger the exception.
try

{
for(int i = 0; i < 10; i++)
{
myCar. Accelerate(10);
}
}
catch(Exception e)
{
// Handle the thrown exception. Console.WriteLine("\n Error! "); Console.WriteLine("Method: {0}", e.TargetSite);
Console.WriteLine("Message: {0}", e.Message);
Console.WriteLine("Source: {0}", e.Source);
}
// The error has been handled, processing continues with the next statement. Console.WriteLine("\n Out of exception logic "); Console.ReadLine();

In essence, a try block is a section of statements that may throw an exception during execution. If an exception is detected, the flow of program execution is sent to the appropriate catch block. On the other hand, if the code within a try block does not trigger an exception, the catch block is skipped entirely, and all is right with the world. The following output shows a test run of this program:
本质上,try 块是在执行过程中可能引发异常的语句部分。如果检测到异常,程序执行流将发送到相应的 catch 块。另一方面,如果 try 块中的代码没有触发异常,则完全跳过 catch 块,世界一切正常。以下输出显示了此程序的测试运行:

Simple Exception Example
=> Creating a car and stepping on it! Jamming…
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100

Error!
Method: Void Accelerate(Int32) Message: Zippy has overheated! Source: SimpleException

Out of exception logic

As you can see, after an exception has been handled, the application is free to continue from the point after the catch block. In some circumstances, a given exception could be critical enough to warrant the termination of the application. However, in a good number of cases, the logic within the exception handler will ensure the application can continue on its merry way (although it could be slightly less functional, such as not being able to connect to a remote data source).
如您所见,在处理异常后,应用程序可以从 catch 块之后的点继续。在某些情况下,给定的例外可能足够严重,足以保证终止申请。但是,在很多情况下,异常处理程序中的逻辑将确保应用程序可以继续其快乐的方式(尽管它的功能可能略低,例如无法连接到远程数据源)。

Throw As Expression (New 7.0)

投掷为表达式(新 7.0)

Prior to C# 7, throw() was a statement, which meant you could throw an exception only where statements are allowed. With C# 7.0 and later, throw() is available as an expression as well and can be called anywhere expressions are allowed.
在 C# 7 之前,throw() 是一个语句,这意味着只有在允许语句的情况下才能引发异常。在 C# 7.0 及更高版本中,throw() 也可以作为表达式使用,并且可以在任何允许表达式的地方调用。

Configuring the State of an Exception

配置异常的状态

Currently, the System.Exception object configured within the Accelerate() method simply establishes a value exposed to the Message property (via a constructor parameter). As shown previously in Table 7-1, however, the Exception class also supplies a number of additional members (TargetSite, StackTrace, HelpLink, and Data) that can be useful in further qualifying the nature of the problem. To spruce up the current example, let’s examine further details of these members on a case-by-case basis.
目前,在 Accelerate() 方法中配置的 System.Exception 对象只是建立向 Message 属性公开的值(通过构造函数参数)。但是,如前面的表 7-1 所示,Exception 类还提供了许多其他成员(TargetSite、StackTrace、HelpLink 和 Data),这些成员可用于进一步限定问题的性质。为了美化当前示例,让我们逐个检查这些成员的更多详细信息。

The TargetSite Property

目标站点属性

The System.Exception.TargetSite property allows you to determine various details about the method that threw a given exception. As shown in the previous code example, printing the value of TargetSite will display the return type, name, and parameter types of the method that threw the exception. However, TargetSite does not return just a vanilla-flavored string but rather a strongly typed System.Reflection. MethodBase object. This type can be used to gather numerous details regarding the offending method, as well as the class that defines the offending method. To illustrate, assume the previous catch logic has been updated as follows:
属性允许您确定有关引发给定异常的方法的各种详细信息。如前面的代码示例所示,打印 TargetSite 的值将显示引发异常的方法的返回类型、名称和参数类型。但是,TargetSite 不仅返回香草味的字符串,还返回强类型的 System.Reflection。方法库对象。此类型可用于收集有关违规方法以及定义违规方法的类的大量详细信息。为了说明这一点,假设前面的 catch 逻辑已更新,如下所示:


// TargetSite actually returns a MethodBase object. catch(Exception e)
{
Console.WriteLine("\n Error! "); Console.WriteLine("Member name: {0}", e.TargetSite);
Console.WriteLine("Class defining member: {0}", e.TargetSite.DeclaringType); Console.WriteLine("Member type: {0}", e.TargetSite.MemberType); Console.WriteLine("Message: {0}", e.Message);
Console.WriteLine("Source: {0}", e.Source);
}
Console.WriteLine("\n Out of exception logic "); Console.ReadLine();

This time, you use the MethodBase.DeclaringType property to determine the fully qualified name of the class that threw the error (SimpleException.Car, in this case) as well as the MemberType property of the MethodBase object to identify the type of member (such as a property versus a method) where this exception originated. In this case, the catch logic will display the following:
这一次,使用 MethodBase.DeclaringType 属性来确定引发错误的类的完全限定名(在本例中为 SimpleException.Car),并使用 MethodBase 对象的 MemberType 属性来标识产生此异常的成员类型(如属性与方法)。在这种情况下,catch 逻辑将显示以下内容:

Error!
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car Member type: Method
Message: Zippy has overheated! Source: SimpleException

The StackTrace Property

堆栈跟踪属性

The System.Exception.StackTrace property allows you to identify the series of calls that resulted in the exception. Be aware that you never set the value of StackTrace, as it is established automatically at the time the exception is created. To illustrate, assume you have once again updated your catch logic.
属性允许您标识导致异常的一系列调用。请注意,您永远不会设置 StackTrace 的值,因为它是在创建异常时自动建立的。为了说明这一点,假设您再次更新了捕获逻辑。

catch(Exception e)
{

Console.WriteLine("Stack: {0}", e.StackTrace);
}

If you were to run the program, you would find the following stack trace is printed to the console (your line numbers and file paths may differ, of course):
如果要运行该程序,您会发现以下堆栈跟踪已打印到控制台(当然,您的行号和文件路径可能会有所不同):

Stack: at SimpleException.Car.Accelerate(Int32 delta)
in [path to file]\car.cs:line 57 at $.

$(String[] args) in [path to file]\Program.cs:line 20

The string returned from StackTrace documents the sequence of calls that resulted in the throwing of this exception. Notice how the bottommost line number of this string identifies the first call in the
sequence, while the topmost line number identifies the exact location of the offending member. Clearly, this information can be quite helpful during the debugging or logging of a given application, as you are able to “follow the flow” of the error’s origin.
从 StackTrace 返回的字符串记录了导致引发此异常的调用序列。请注意此字符串最底部的行号如何标识序列,而最上面的行号标识违规成员的确切位置。显然,在调试或记录给定应用程序期间,此信息非常有用,因为您可以“跟踪错误来源的流程”。

The HelpLink Property

帮助链接属性

While the TargetSite and StackTrace properties allow programmers to gain an understanding of a given exception, this information is of little use to the end user. As you have already seen, the System.Exception. Message property can be used to obtain human-readable information that can be displayed to the current user. In addition, the HelpLink property can be set to point the user to a specific URL or standard help file that contains more detailed information.
虽然 TargetSite 和 StackTrace 属性允许程序员了解给定的异常,但此信息对最终用户几乎没有用处。正如您已经看到的,System.Exception。消息属性可用于获取可显示给当前用户的人类可读信息。此外,还可以将 HelpLink 属性设置为将用户指向包含更多详细信息的特定 URL 或标准帮助文件。

By default, the value managed by the HelpLink property is an empty string. Update the exception using object initialization to provide a more interesting value. Here are the relevant updates to the Car. Accelerate() method:
默认情况下,由帮助链接属性管理的值为空字符串。使用对象初始化更新异常以提供更有趣的值。以下是汽车的相关更新。加速() 方法:

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order…", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
CurrentSpeed = 0;
_carIsDead = true;

// Use the "throw" keyword to raise an exception and
// return to the caller.
throw new Exception($"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com"
};
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

The catch logic could now be updated to print this help-link information as follows:

catch(Exception e)
{

Console.WriteLine("Help Link: {0}", e.HelpLink);
}

The Data Property

数据属性

The Data property of System.Exception allows you to fill an exception object with relevant auxiliary information (such as a timestamp). The Data property returns an object implementing an interface named IDictionary, defined in the System.Collections namespace. Chapter 8 examines the role of interface- based programming, as well as the System.Collections namespace. For the time being, just understand that dictionary collections allow you to create a set of values that are retrieved using a specific key. Observe the next update to the Car.Accelerate() method:
System.Exception 的 Data 属性允许您使用相关的辅助信息(如时间戳)填充异常对象。Data 属性返回一个对象,该对象实现在 System.Collections 命名空间中定义的名为 IDictionary 的接口。第8章探讨了基于接口的编程以及System.Collections命名空间的作用。目前,只需了解字典集合允许您创建一组使用特定键检索的值。观察 Car.Accelerate() 方法的下一次更新:

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order…", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
Console.WriteLine("{0} has overheated!", PetName); CurrentSpeed = 0;
_carIsDead = true;
// Use the "throw" keyword to raise an exception
// and return to the caller.
throw new Exception($"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com", Data = {
{"TimeStamp",$"The car exploded at {DateTime.Now}"},
{"Cause","You have a lead foot."}
}

};
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

To successfully enumerate over the key-value pairs, make sure you added a using directive for the System.Collections namespace since you will use a DictionaryEntry type in the file containing the class implementing your top-level statements.
若要成功枚举键值对,请确保为 System.Collections 命名空间添加了 using 指令,因为您将在包含实现顶级语句的类的文件中使用 DictionaryEntry 类型。

using System.Collections;

Next, you need to update the catch logic to test that the value returned from the Data property is not null (the default value). After that, you use the Key and Value properties of the DictionaryEntry type to print the custom data to the console.
接下来,需要更新 catch 逻辑以测试从 Data 属性返回的值是否不为 null(默认值)。之后,您可以使用 DictionaryEntry 类型的键和值属性将自定义数据打印到控制台。

catch (Exception e)
{

Console.WriteLine("\n-> Custom Data:"); foreach (DictionaryEntry de in e.Data)
{
Console.WriteLine("-> {0}: {1}", de.Key, de.Value);
}
}

With this, here’s the final output you’d see:
有了这个,这是您将看到的最终输出:

Simple Exception Example
=> Creating a car and stepping on it! Jamming…
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
Error!
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car Member type: Method
Message: Zippy has overheated! Source: SimpleException
Stack: at SimpleException.Car.Accelerate(Int32 delta) … at SimpleException.Program.Main(String[] args) …
Help Link: http://www.CarsRUs.com

-> Custom Data:
-> TimeStamp: The car exploded at 3/15/2020 16:22:59
-> Cause: You have a lead foot.

Out of exception logic

The Data property is useful in that it allows you to pack in custom information regarding the error at hand, without requiring the building of a new class type to extend the Exception base class. As helpful as the Data property may be, however, it is still common for developers to build strongly typed exception classes, which handle custom data using strongly typed properties.
Data 属性非常有用,因为它允许您打包有关当前错误的自定义信息,而无需生成新的类类型来扩展 Exception 基类。但是,尽管 Data 属性可能很有帮助,但开发人员仍然很常见地生成强类型异常类,这些异常类使用强类型属性处理自定义数据。

This approach allows the caller to catch a specific exception-derived type, rather than having to dig into a data collection to obtain additional details. To understand how to do this, you need to examine the distinction between system-level and application-level exceptions.
Data 属性非常有用,因为它允许您打包有关当前错误的自定义信息,而无需生成新的类类型来扩展 Exception 基类。但是,尽管 Data 属性可能很有帮助,但开发人员仍然很常见地生成强类型异常类,这些异常类使用强类型属性处理自定义数据。

System-Level Exceptions (System.SystemException)

系统级异常(System.SystemException)

The .NET base class libraries define many classes that ultimately derive from System.Exception. For example, the System namespace defines core exception objects such as ArgumentOutOfRangeException, IndexOutOfRangeException, StackOverflowException, and so forth. Other namespaces define exceptions that reflect the behavior of that namespace. For example, System.Drawing.Printing defines printing exceptions, System.IO defines input/output-based exceptions, System.Data defines database-centric exceptions, and so forth.
.NET 基类库定义了许多最终派生自 System.Exception 的类。例如,命名空间定义核心异常对象,如 ArgumentOutOfRangeException、IndexOutOfRangeException、StackOverflowException 等。其他命名空间定义反映该命名空间行为的异常。例如,System.Drawing.Print 定义打印异常,System.IO 定义基于输入/输出的异常,System.Data 定义以数据库为中心的异常,等等。

Exceptions that are thrown by the .NET platform are (appropriately) called system exceptions. These exceptions are generally regarded as nonrecoverable, fatal errors. System exceptions derive directly from a base class named System.SystemException, which in turn derives from System.Exception (which derives from System.Object).
.NET 平台引发的异常(适当地)称为系统异常。这些异常通常被视为不可恢复的致命错误。系统异常直接派生自名为 System.SystemException 的基类,而基类又派生自 System.Exception(派生自 System.Object)。

public class SystemException : Exception
{
// Various constructors.
}

Given that the System.SystemException type does not add any functionality beyond a set of custom constructors, you might wonder why SystemException exists in the first place. Simply put, when an exception type derives from System.SystemException, you are able to determine that the .NET runtime is the entity that has thrown the exception, rather than the code base of the executing application. You can verify this quite simply using the is keyword.
鉴于 System.SystemException 类型除了一组自定义构造函数之外不会添加任何功能,您可能想知道为什么 SystemException 首先存在。简而言之,当异常类型派生自 System.SystemException 时,您可以确定 .NET 运行时是引发异常的实体,而不是正在执行的应用程序的代码库。您可以使用 is 关键字非常简单地验证这一点。

// True! NullReferenceException is-a SystemException. NullReferenceException nullRefEx = new NullReferenceException(); Console.WriteLine(
"NullReferenceException is-a SystemException? : {0}", nullRefEx is SystemException);

Application-Level Exceptions (System.ApplicationException)

应用程序级异常(System.ApplicationException)

Given that all .NET exceptions are class types, you are free to create your own application-specific exceptions. However, because the System.SystemException base class represents exceptions thrown from the runtime, you might naturally assume that you should derive your custom exceptions from the System.Exception type. You could do this, but you could instead derive from the System. ApplicationException class.
鉴于所有 .NET 异常都是类类型,您可以自由创建自己的特定于应用程序的异常。但是,由于 System.SystemException 基类表示从运行时引发的异常,因此您可能自然而然地假定应派生自定义异常从系统异常类型。您可以这样做,但您可以从系统派生。应用程序异常类。

public class ApplicationException : Exception
{
// Various constructors.
}

Like SystemException, ApplicationException does not define any additional members beyond a set of constructors. Functionally, the only purpose of System.ApplicationException is to identify the source of the error. When you handle an exception deriving from System.ApplicationException, you can assume the exception was raised by the code base of the executing application, rather than by the .NET Core base class libraries or .NET runtime engine.
与 SystemException 一样,ApplicationException 除了一组构造函数之外,不会定义任何其他成员。从功能上讲,System.ApplicationException 的唯一目的是识别错误的源。处理从 System.ApplicationException 派生的异常时,可以假定异常是由执行应用程序的代码库引发的,而不是由 .NET Core 基类库或 .NET 运行时引擎引发的。

Building Custom Exceptions, Take 1

构建自定义异常,采用 1

While you can always throw instances of System.Exception to signal a runtime error (as shown in the first example), it is sometimes advantageous to build a strongly typed exception that represents the unique details of your current problem. For example, assume you want to build a custom exception (named CarIsDeadException) to represent the error of speeding up a doomed automobile. The first step is to derive a new class from System.Exception/System.ApplicationException (by convention, all exception class names end with the Exception suffix).
虽然您始终可以抛出 System.Exception 的实例来发出运行时错误的信号(如第一个示例中所示),但有时构建一个表示当前问题的唯一详细信息的强类型异常是有利的。例如,假设您要构建一个自定义异常(名为CarIsDeadException)来表示加速注定要失败的汽车的错误。第一步是从 System.Exception/System.ApplicationException 派生一个新类(按照惯例,所有异常类名都以 Exception 后缀结尾)。

■ Note as a rule, all custom exception classes should be defined as public classes (recall that the default access modifier of a non-nested type is internal). The reason is that exceptions are often passed outside of assembly boundaries and should therefore be accessible to the calling code base.
请注意,作为一项规则,所有自定义异常类都应定义为公共类(回想一下,非嵌套类型的默认访问修饰符是内部的)。原因是异常通常在程序集边界之外传递,因此调用代码库应该可以访问。

Create a new Console Application project named CustomException, copy the previous Car.cs and Radio.cs files into your new project, and change the namespace that defines the Car and Radio types from SimpleException to CustomException. Next, add a new file named CarIsDeadException.cs and add the following class definition:
创建一个名为 CustomException 的新控制台应用程序项目,将以前的 Car.cs 和 Radio.cs 文件复制到新项目中,并将定义 Car 和 Radio 类型的命名空间从 SimpleException 更改为 CustomException。接下来,添加一个名为 CarIsDeadException 的新文件.cs并添加以下类定义:

namespace CustomException;
// This custom exception describes the details of the car-is-dead condition.
// (Remember, you can also simply extend Exception.) public class CarIsDeadException : ApplicationException
{
}

As with any class, you are free to include any number of custom members that can be called within the catch block of the calling logic. You are also free to override any virtual members defined by your parent classes. For example, you could implement CarIsDeadException by overriding the virtual Message property.
与任何类一样,您可以自由地包含任意数量的自定义成员,这些成员可以在调用逻辑的 catch 块中调用。您还可以自由覆盖父类定义的任何虚拟成员。例如,您可以通过重写虚拟消息属性来实现 CarIsDeadException。

As well, rather than populating a data dictionary (via the Data property) when throwing the exception, the constructor allows the sender to pass in a timestamp and reason for the error. Finally, the timestamp data and cause of the error can be obtained using strongly typed properties.
同样,构造函数允许发送方传入时间戳和错误原因,而不是在引发异常时填充数据字典(通过 Data 属性)。最后,可以使用强类型属性获取时间戳数据和错误原因。

public class CarIsDeadException : ApplicationException
{
private string _messageDetails = String.Empty; public DateTime ErrorTimeStamp {get; set;} public string CauseOfError {get; set;}

public CarIsDeadException(){}
public CarIsDeadException(string message, string cause, DateTime time)
{
_messageDetails = message; CauseOfError = cause; ErrorTimeStamp = time;
}

// Override the Exception.Message property. public override string Message
=> $"Car Error Message: {_messageDetails}";
}

Here, the CarIsDeadException class maintains a private field (_messageDetails) that represents data regarding the current exception, which can be set using a custom constructor. Throwing this exception from the Accelerate() method is straightforward. Simply allocate, configure, and throw a CarIsDeadException type rather than a System.Exception.
在这里,CarIsDeadException 类维护一个私有字段 (_messageDetails),该字段表示有关当前异常的数据,可以使用自定义构造函数进行设置。从 Accelerate() 方法引发此异常非常简单。只需分配、配置和抛出 CarIsDeadException 类型,而不是 System.Exception。

// Throw the custom CarIsDeadException. public void Accelerate(int delta)
{

throw new CarIsDeadException(
$"{PetName} has overheated!",
"You have a lead foot", DateTime.Now )
{
HelpLink = "http://www.CarsRUs.com",
};

}

To catch this incoming exception, your catch scope can now be updated to catch a specific CarIsDeadException type (however, given that CarIsDeadException “is-a” System.Exception, it is still permissible to catch a System.Exception as well).
要捕获此传入异常,现在可以更新捕获范围以捕获特定的CarIsDeadException类型(但是,鉴于CarIsDeadException“是-a”System.Exception,仍然允许捕获System.Exception)。

using CustomException;

Console.WriteLine(" Fun with Custom Exceptions \n"); Car myCar = new Car("Rusty", 90);

try
{
// Trip exception. myCar.Accelerate(50);
}
catch (CarIsDeadException e)

{
Console.WriteLine(e.Message); Console.WriteLine(e.ErrorTimeStamp); Console.WriteLine(e.CauseOfError);
}
Console.ReadLine();

So, now that you understand the basic process of building a custom exception, it’s time to build on that knowledge.
因此,现在您已经了解了构建自定义异常的基本过程,是时候基于这些知识进行构建了。

Building Custom Exceptions, Take 2

构建自定义异常,采用 2

The current CarIsDeadException type has overridden the virtual System.Exception.Message property to configure a custom error message and has supplied two custom properties to account for additional bits of data. In reality, however, you are not required to override the virtual Message property, as you could simply pass the incoming message to the parent’s constructor as follows:
当前的 CarIsDeadException 类型已覆盖虚拟 System.Exception.Message 属性以配置自定义错误消息,并提供了两个自定义属性来考虑其他数据位。但是,实际上,您不需要重写虚拟 Message 属性,因为您只需将传入消息传递给父级的构造函数,如下所示:

public class CarIsDeadException : ApplicationException
{
public DateTime ErrorTimeStamp { get; set; } public string CauseOfError { get; set; }

public CarIsDeadException() { }

// Feed message to parent constructor.
public CarIsDeadException(string message, string cause, DateTime time)
:base(message)
{
CauseOfError = cause;
ErrorTimeStamp = time;
}
}

Notice that this time you have not defined a string variable to represent the message and have not overridden the Message property. Rather, you are simply passing the parameter to your base class constructor. With this design, a custom exception class is little more than a uniquely named class deriving from System. ApplicationException (with additional properties if appropriate), devoid of any base class overrides.
请注意,这次您没有定义一个字符串变量来表示消息,也没有重写 Message 属性。相反,您只需将参数传递给基类构造函数。通过这种设计,自定义异常类只不过是从 System 派生的唯一命名类。ApplicationException(如果适用,具有其他属性),没有任何基类重写。

Don’t be surprised if most (if not all) of your custom exception classes follow this simple pattern. Many times, the role of a custom exception is not necessarily to provide additional functionality beyond what is inherited from the base classes but to supply a strongly named type that clearly identifies the nature of the error so the client can provide different handler logic for different types of exceptions.
如果大多数(如果不是全部)自定义异常类都遵循这种简单的模式,请不要感到惊讶。很多时候,自定义异常的作用不一定是提供从基类继承的功能之外的其他功能,而是提供明确标识错误性质的强名称类型,以便客户端可以为不同类型的异常提供不同的处理程序逻辑。

Building Custom Exceptions, Take 3

构建自定义异常,采用 3

If you want to build a truly prim-and-proper custom exception class, you want to make sure your custom exception does the following:
如果要生成一个真正原始且正确的自定义异常类,则需要确保自定义异常执行以下操作:
• Derives from Exception/ApplicationException
派生自异常/应用程序异常
• Defines a default constructor
定义默认构造函数
•Defines a constructor that sets the inherited Message property
定义设置继承的 Message 属性的构造函数
•Defines a constructor to handle “inner exceptions”
定义用于处理“内部异常”的构造函数

To complete your examination of building custom exceptions, here is the final iteration of CarIsDeadException, which accounts for each of these special constructors (the properties would be as shown in the previous example):
为了完成对生成自定义异常的检查,下面是 CarIsDeadException 的最终迭代,它考虑了这些特殊构造函数中的每一个(属性如前面的示例所示):

public class CarIsDeadException : ApplicationException
{
private string _messageDetails = String.Empty; public DateTime ErrorTimeStamp {get; set;} public string CauseOfError {get; set;}

public CarIsDeadException(){}
public CarIsDeadException(string cause, DateTime time) : this(cause,time,string.Empty)
{
}
public CarIsDeadException(string cause, DateTime time, string message) : this(cause,time,message, null)
{
}

public CarIsDeadException(string cause, DateTime time, string message, System. Exception inner)
: base(message, inner)
{
CauseOfError = cause;
ErrorTimeStamp = time;
}
}

With this update to your custom exception, update the Accelerate() method to the following:
通过对自定义异常的此更新,将 Accelerate() 方法更新为以下内容:

throw new CarIsDeadException("You have a lead foot", DateTime.Now,$"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com",
};

Given that building custom exceptions that adhere to .NET Core best practices really differ by only their name, you will be happy to know that Visual Studio provides a code snippet template named Exception that will autogenerate a new exception class that adheres to .NET best practices. To activate it, type exc in the editor and hit the Tab key (in Visual Studio, hit the Tab key twice).
鉴于生成遵循 .NET Core 最佳做法的自定义异常实际上仅在名称上有所不同,您会很高兴知道 Visual Studio 提供了一个名为 Exception 的代码段模板,该模板将自动生成符合 .NET 最佳做法的新异常类。要激活它,请在编辑器中键入 exc 并按 Tab 键(在 Visual Studio 中,按 Tab 键两次)。

Multiple Exceptions

处理多个异常

In its simplest form, a try block has a single catch block. In reality, though, you often run into situations where the statements within a try block could trigger numerous possible exceptions. Create a new
C# Console Application project named ProcessMultipleExceptions; copy the Car.cs, Radio.cs, and CarIsDeadException.cs files from the previous CustomException example into the new project, and update your namespace names accordingly.
在最简单的形式中,try 块只有一个 catch 块。但是,在现实中,您经常会遇到 try 块中的语句可能会触发许多可能的异常的情况。创建一个新的名为 ProcessMultipleExceptions 的 C# 控制台应用程序项目;将 Car.cs、Radio.cs 和 CarIsDeadException.cs 文件从以前的 CustomException 示例复制到新项目中,并相应地更新命名空间名称。

Now, update Car’s Accelerate() method to also throw a predefined base class library ArgumentOutOfRangeException if you pass an invalid parameter (which you can assume is any value less than zero). Note the constructor of this exception class takes the name of the offending argument as the first string, followed by a message describing the error.
现在,更新 Car 的 Accelerate() 方法,以便在传递无效参数(可以假定该参数小于零的任何值)时也抛出预定义的基类库 ArgumentOutOfRangeException。请注意,此异常类的构造函数将有问题的参数的名称作为第一个字符串,后跟描述错误的消息。

// Test for invalid argument before proceeding. public void Accelerate(int delta)
{
if (delta < 0)
{
throw new ArgumentOutOfRangeException(nameof(delta), "Speed must be greater than zero");
}

}

■ Note The nameof() operator returns a string representing the name of the object, in this example the variable delta. This is a safer way to refer to C# objects, methods, and variables when the string version is required.
注意 nameof() 运算符返回一个表示对象名称的字符串,在本例中为变量 delta。当需要字符串版本时,这是引用 C# 对象、方法和变量的更安全方法。

The catch logic could now specifically respond to each type of exception.
catch 逻辑现在可以专门响应每种类型的异常。

using ProcessMultipleExceptions;

Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
// Trip Arg out of range exception. myCar.Accelerate(-10);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

When you are authoring multiple catch blocks, you must be aware that when an exception is thrown, it will be processed by the first appropriate catch. To illustrate exactly what the “first appropriate” catch means, assume you retrofitted the previous logic with an additional catch scope that attempts to handle all exceptions beyond CarIsDeadException and ArgumentOutOfRangeException by catching a general System. Exception as follows:
创作多个 catch 块时,必须注意,当引发异常时,它将由第一个适当的 catch 处理。为了准确说明“第一个适当的”捕获的含义,假设您使用尝试处理所有捕获的附加捕获范围来改造前面的逻辑CarIsDeadException 和 ArgumentOutOfRangeException 之外的异常,通过捕获一般系统。例外情况如下:

// This code will not compile!
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);

try
{
// Trigger an argument out of range exception. myCar.Accelerate(-10);
}
catch(Exception e)
{
// Process all other exceptions? Console.WriteLine(e.Message);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

This exception handling logic generates compile-time errors. The problem is that the first catch block can handle anything derived from System.Exception (given the “is-a” relationship), including the CarIsDeadException and ArgumentOutOfRangeException types. Therefore, the final two catch blocks are unreachable!
此异常处理逻辑生成编译时错误。问题在于,第一个 catch 块可以处理从 System.Exception 派生的任何内容(给定“is-a”关系),包括 CarIsDeadException 和 ArgumentOutOfRangeException 类型。因此,最后两个捕获块是无法访问的!

The rule of thumb to keep in mind is to make sure your catch blocks are structured such that the first catch is the most specific exception (i.e., the most derived type in an exception-type inheritance chain), leaving the final catch for the most general (i.e., the base class of a given exception inheritance chain, in this case System.Exception).
要记住的经验法则是确保你的 catch 块的结构使得第一个 catch 是最具体的异常(即异常类型继承链中派生最多的类型),将最终的 catch 留给最一般的(即,给定异常继承链的基类,在本例中为 System.Exception)。

Thus, if you want to define a catch block that will handle any errors beyond CarIsDeadException and
ArgumentOutOfRangeException, you could write the following:
因此,如果你想定义一个捕获块来处理CarIsDeadException之外的任何错误,并且ArgumentOutOfRangeException,你可以写以下内容:

// This code compiles just fine.
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
// Trigger an argument out of range exception. myCar.Accelerate(-10);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}

// This will catch any other exception
// beyond CarIsDeadException or
// ArgumentOutOfRangeException. catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

■ Note Where at all possible, always favor catching specific exception classes, rather than a general System.Exception. Though it might appear to make life simple in the short term (you may think “ah! This catches all the other things I don’t care about.”), in the long term you could end up with strange runtime crashes, as a more serious error was not directly dealt with in your code. remember, a final catch block that deals with System.Exception tends to be very general indeed.
注意 在可能的情况下,始终倾向于捕获特定的异常类,而不是一般的 System.Exception。虽然它可能在短期内看起来使生活变得简单(你可能会想“啊!这抓住了我不关心的所有其他事情“),从长远来看,你最终可能会遇到奇怪的运行时崩溃,因为更严重的错误没有在你的代码中直接处理。请记住,处理System.Exception的最后一个捕获块确实非常通用。

General catch Statements

一般捕获声明

C# also supports a “general” catch scope that does not explicitly receive the exception object thrown by a given member.
C# 还支持“常规”捕获范围,该范围不显式接收给定成员引发的异常对象。

// A generic catch.
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
myCar.Accelerate(90);
}
catch
{
Console.WriteLine("Something bad happened…");
}
Console.ReadLine();

Obviously, this is not the most informative way to handle exceptions since you have no way to obtain meaningful data about the error that occurred (such as the method name, call stack, or custom message). Nevertheless, C# does allow for such a construct, which can be helpful when you want to handle all errors in a general fashion.
显然,这不是处理异常的最有用的方法,因为您无法获取有关所发生错误的有意义的数据(例如方法名称、调用堆栈或自定义消息)。尽管如此,C# 确实允许这样的构造,当您希望以常规方式处理所有错误时,这会很有帮助。

Rethrowing Exceptions

重新引发异常

When you catch an exception, it is permissible for the logic in a try block to rethrow the exception up the call stack to the previous caller. To do so, simply use throw() within a catch block. This passes the exception up the chain of calling logic, which can be helpful if your catch block is only able to partially handle the error at hand.
捕获异常时,允许 try 块中的逻辑将异常重新抛出到调用堆栈中到前一个调用方。为此,只需在 catch 块中使用 throw()。 这会将异常传递到调用逻辑链上,如果您的 catch 块只能部分处理手头的错误,这会很有帮助。

// Passing the buck.

try
{
// Speed up car logic…
}
catch(CarIsDeadException e)
{
// Do any partial processing of this error and pass the buck. throw;
}

Be aware that, in this example code, the ultimate receiver of CarIsDeadException is the .NET runtime because it is the top-level statements rethrowing the exception. Because of this, your end user is presented with a system-supplied error dialog box. Typically, you would only rethrow a partial handled exception to a caller that has the ability to handle the incoming exception more gracefully.
请注意,在此示例代码中,CarIsDeadException 的最终接收方是 .NET 运行时,因为它是重新引发异常的顶级语句。因此,最终用户会看到系统提供的错误对话框。通常,您只会将部分处理的异常重新引发给能够更优雅地处理传入异常的调用方。

Notice as well that you are not explicitly rethrowing the CarIsDeadException object but rather making use of throw() with no argument. You’re not creating a new exception object; you’re just rethrowing
the original exception object (with all its original information). Doing so preserves the context of the original target.
还要注意的是,你没有显式地重新抛出 CarIsDeadException 对象,而是在没有参数的情况下使用 throw()。 您没有创建新的异常对象;你只是在重新投掷原始异常对象(及其所有原始信息)。这样做可以保留原始目标的上下文。

Inner Exceptions

内部异常

As you might suspect, it is entirely possible to trigger an exception at the time you are handling another exception. For example, assume you are handling a CarIsDeadException within a particular catch scope and during the process you attempt to record the stack trace to a file on your C: drive named carErrors. txt (the implicit global using statements grant you access to the System.IO namespace and its I/O- centric types).
正如您可能怀疑的那样,完全有可能在处理另一个异常时触发异常。例如,假设您正在特定的 catch 范围内处理 CarIsDeadException,并且在此过程中尝试将堆栈跟踪记录到 C: 驱动器上名为 carErrors 的文件.txt (隐式全局 using 语句授予您访问 System.IO 命名空间及其以 I/O 为中心的类型)。

catch(CarIsDeadException e)
{
// Attempt to open a file named carErrors.txt on the C drive. FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);

}

Now, if the specified file is not located on your C: drive, the call to File.Open() results in a FileNotFoundException! Later in this book, you will learn all about the System.IO namespace where you’ll discover how to programmatically determine whether a file exists on the hard drive before attempting to open the file in the first place (thereby avoiding the exception altogether). However, to stay focused on the topic of exceptions, assume the exception has been raised.
现在,如果指定的文件不在 C: 驱动器上,则调用 File.Open() 会导致 FileNotFoundException!在本书的后面部分,您将了解有关 System.IO 命名空间的所有信息,您将在其中了解如何在尝试打开文件之前以编程方式确定硬盘驱动器上是否存在文件(从而完全避免异常)。但是,为了继续关注异常主题,请假设已引发异常。

When you encounter an exception while processing another exception, best practice states that you should record the new exception object as an “inner exception” within a new object of the same type as the initial exception. (That was a mouthful!) The reason you need to allocate a new object of the exception being handled is that the only way to document an inner exception is via a constructor parameter. Consider the following code:
现在,如果指定的文件不在 C: 驱动器上,则调用 File.Open() 会导致 FileNotFoundException!在本书的后面部分,您将了解有关 System.IO 命名空间的所有信息,您将在其中了解如何在尝试打开文件之前以编程方式确定硬盘驱动器上是否存在文件(从而完全避免异常)。但是,为了继续关注异常主题,请假设已引发异常。

//Update the exception handler catch (CarIsDeadException e)
{

try
{
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);

}
catch (Exception e2)
{
//This causes a compile error-InnerException is read only
//e.InnerException = e2;
// Throw an exception that records the new exception,
// as well as the message of the first exception.
throw new CarIsDeadException( e.CauseOfError, e.ErrorTimeStamp, e.Message, e2); }
}

Notice, in this case, I have passed in the FileNotFoundException object as the fourth parameter to the CarIsDeadException constructor. After you have configured this new object, you throw it up the call stack to the next caller, which in this case would be the top-level statements.
请注意,在本例中,我已经将FileNotFoundException对象作为第四个参数传递给CarIsDeadException构造函数。配置此新对象后,将其将调用堆栈抛出给下一个调用方,在本例中为顶级语句。

Given that there is no “next caller” after the top-level statements to catch the exception, you would be again presented with an error dialog box. Much like the act of rethrowing an exception, recording inner exceptions is usually useful only when the caller has the ability to gracefully catch the exception in the first place. If this is the case, the caller’s catch logic can use the InnerException property to extract the details of the inner exception object.
鉴于顶级语句后没有“下一个调用方”来捕获异常,您将再次看到一个错误对话框。与重新引发异常的行为非常相似,记录内部异常通常仅在调用方能够首先优雅地捕获异常时才有用。如果是这种情况,调用方的 catch 逻辑可以使用 InnerException 属性提取内部异常对象的详细信息。

The finally Block

最后的街区
A try/catch scope may also define an optional finally block. The purpose of a finally block is to ensure that a set of code statements will always execute, exception (of any type) or not. To illustrate, assume you want to always power down the car’s radio before exiting the program, regardless of any handled exception.
try/catch 范围也可以定义一个可选的 finally 块。finally 块的目的是确保一组代码语句始终执行,无论是否异常(任何类型的)。为了说明这一点,假设您希望始终在退出程序之前关闭汽车的收音机,而不管任何处理的异常。

Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
myCar.CrankTunes(true); try
{
// Speed up car logic.
}
catch(CarIsDeadException e)
{
// Process CarIsDeadException.
}
catch(ArgumentOutOfRangeException e)
{
// Process ArgumentOutOfRangeException.
}
catch(Exception e)
{
// Process any other Exception.
}

finally
{
// This will always occur. Exception or not. myCar.CrankTunes(false);
}
Console.ReadLine();

If you did not include a finally block, the radio would not be turned off if an exception were encountered (which might or might not be problematic). In a more real-world scenario, when you need to dispose of objects, close a file, or detach from a database (or whatever), a finally block ensures a location for proper cleanup.
如果未包含 finally 块,则在遇到异常时不会关闭无线电(可能会也可能没有问题)。在更真实的场景中,当您需要释放对象、关闭文件或从数据库分离(或其他任何内容)时,finally 块可确保正确清理的位置。

Exception Filters

异常筛选器

C# 6 introduced a new clause that can be placed on a catch scope, via the when keyword. When you add this clause, you have the ability to ensure that the statements within a catch block are executed only if some condition in your code holds true. This expression must evaluate to a Boolean (true or false) and can be obtained by using a simple code statement in the when definition itself or by calling an additional method in your code. In a nutshell, this approach allows you to add “filters” to your exception logic.
C# 6 引入了一个新子句,可以通过 when 关键字将其放置在 catch 作用域中。添加此子句时,能够确保仅当代码中的某些条件成立时,才执行 catch 块中的语句。此表达式的计算结果必须为布尔值(真或假),可以通过在 when 定义本身中使用简单的代码语句或在代码中调用其他方法来获取。简而言之,此方法允许您向异常逻辑添加“筛选器”。

Consider the following modified exception logic. I have added a when clause to the CarIsDeadException handler to ensure the catch block is never executed on a Friday (a contrived example, but who wants their automobile to break down right before the weekend?). Notice that the single Boolean statement in the when clause must be wrapped in parentheses.
请考虑以下修改后的异常逻辑。我在 CarIsDeadException 处理程序中添加了一个 when 子句,以确保 catch 块永远不会在星期五执行(一个人为的例子,但谁希望他们的汽车在周末之前发生故障?请注意,when 子句中的单个布尔语句必须括在括号中。

catch (CarIsDeadException e) when (e.ErrorTimeStamp.DayOfWeek != DayOfWeek.Friday)
{
// This new line will only print if the when clause evaluates to true.
// 仅当 when 子句的计算结果为 true 时,才会打印此新行。
Console.WriteLine("Catching car is dead!");

Console.WriteLine(e.Message);
}

While this example is very contrived, a more realistic use for using an exception filter is to catch SystemExceptions. For example, suppose your code is saving data to the database, a general exception is thrown. By examining the message and exception data, you can create specific handlers based on what caused the exception.
虽然此示例非常人为,但使用异常筛选器的更实际用途是捕获 SystemException。例如,假设您的代码正在将数据保存到数据库,则会引发常规异常。通过检查消息和异常数据,可以根据导致异常的原因创建特定的处理程序。

// Debugging Unhandled Exceptions Using Visual Studio
使用 Visual Studio 调试未经处理的异常

Visual Studio supplies a number of tools that help you debug unhandled exceptions. Assume you have increased the speed of a Car object beyond the maximum but this time did not bother to wrap your call within a try block.
Visual Studio 提供了许多工具来帮助你调试未经处理的异常。假设您已将 Car 对象的速度提高到超过最大值,但这次没有费心将您的调用包装在 try 块中。
Car myCar = new Car("Rusty", 90); myCar.Accelerate(100);

If you start a debugging session within Visual Studio (using the Debug ➤ Start Debugging menu selection), Visual Studio automatically breaks at the time the uncaught exception is thrown. Better yet, you are presented with a window (see Figure 7-1) displaying the value of the Message property.
如果在 Visual Studio 中启动调试会话(使用“调试”➤“启动调试”菜单选项),则在引发未捕获的异常时,Visual Studio 会自动中断。更好的是,您将看到一个窗口(请参阅图 7-1),其中显示了 Message 属性的值。

Alt text

Figure 7-1. Debugging unhandled custom exceptions with Visual Studio
图 7-1。 使用 Visual Studio 调试未经处理的自定义异常

■ Note If you fail to handle an exception thrown by a method in the .neT base class libraries, the Visual studio debugger breaks at the statement that called the offending method.
注意 如果无法处理 .neT 基类库中的方法引发的异常,Visual Studio 调试器将在调用违规方法的语句处中断。

If you click the View Detail link, you will find the details regarding the state of the object (see Figure 7-2).
如果单击“查看详细信息”链接,将找到有关对象状态的详细信息(请参阅图 7-2)。

Alt text
Figure 7-2. Viewing exception details
图 7-2。 查看异常详细信息

Summary

总结

In this chapter, you examined the role of structured exception handling. When a method needs to send an error object to the caller, it will allocate, configure, and throw a specific System.Exception-derived type via throw(). The caller is able to handle any possible incoming exceptions using the C# catch keyword and an optional finally scope. Since C# 6.0, the ability to create exception filters using the optional when keyword was added, and C# 7 has expanded the locations from where you can throw exceptions.
在本章中,您研究了结构化异常处理的作用。当方法需要向调用方发送错误对象时,它将通过 throw() 分配、配置和抛出特定的 System.Exception 派生类型。调用方能够使用 C# catch 关键字和可选的 finally 作用域处理任何可能的传入异常。自 C# 6.0 起,添加了使用可选 when 关键字创建异常筛选器的功能,并且 C# 7 扩展了可以引发异常的位置。

When you are creating your own custom exceptions, you ultimately create a class type deriving from System.ApplicationException, which denotes an exception thrown from the currently executing
application. In contrast, error objects deriving from System.SystemException represent critical (and fatal)
errors thrown by the .NET runtime. Last but not least, this chapter illustrated various tools within Visual Studio that can be used to create custom exceptions (according to .NET best practices) as well as debug exceptions.
在创建自己的自定义异常时,最终会创建一个派生自 System.ApplicationException 的类类型,该类类型表示从当前执行的应用。相反,派生自 System.SystemException 的错误对象表示严重(和严重).NET 运行时引发的错误。最后但并非最不重要的一点是,本章演示了Visual Studio中的各种工具,这些工具可用于创建自定义异常(根据.NET最佳实践)以及调试异常。

发表评论