检查异常和异常处理过程
- 11 分钟
使用称为异常的机制管理 C# 应用程序中的运行时错误。 异常提供一种结构化、统一且类型安全的方法来处理系统级别和应用程序级错误条件。 异常由 .NET 运行时或应用程序中的代码生成。
需要异常处理的常见方案
有几个编程方案需要异常处理。 其中许多方案涉及某种形式的数据收集。 尽管某些方案涉及此训练范围之外的编码技术,但它们仍值得注意。
需要异常处理的常见方案包括:
用户输入:代码处理用户输入时可能发生异常。 例如,当输入值的格式不正确或范围不足时,会发生异常。
数据处理和计算:当代码执行数据计算或转换时,可能会发生异常。 例如,当代码尝试除以零、强制转换为不受支持的类型或赋值超过范围时,会发生异常。
文件输入/输出作:当代码从文件中读取或写入文件时,可能会发生异常。 例如,当文件不存在、程序无权访问该文件或文件被另一个进程使用时,会发生异常。
数据库操作:当代码与数据库交互时,可能会发生异常。 例如,当数据库连接丢失、SQL 语句中发生语法错误或发生约束冲突时,会发生异常。
网络通信:当代码通过网络通信时,可能会发生异常。 例如,当网络连接丢失、超时或远程服务器返回错误时,会发生异常。
其他外部资源:当代码与其他外部资源通信时,可能会发生异常。 由于各种原因,Web 服务、REST API 或第三方库可能会引发异常。 例如,由于网络连接问题、格式不正确的数据等,会出现异常。
异常处理关键字、代码块和模式
C# 中的异常处理是通过使用 try
、catch
和 finally
关键字实现的。 其中每个关键字都有一个关联的代码块,可用于满足异常处理方法中的特定目标。 例如:
try
{
// try code block - code that may generate an exception
}
catch
{
// catch code block - code to handle an exception
}
finally
{
// finally code block - code to clean up resources
}
注释
C# 语言还允许代码使用 throw
关键字生成异常对象。 包括使用 throw
关键字生成异常的异常处理方案在 Microsoft Learn 上的单独模块中介绍。
try
代码块包含可能导致异常的受保护的代码。 如果块中的 try
代码导致异常,则异常由相应的 catch
块处理。
catch
代码块包含捕获异常时执行的代码。 该 catch
块可以处理异常、记录异常或忽略异常。 catch
可以将块配置为在发生任何异常类型时执行,或仅在发生特定类型的异常时执行。
finally
代码块包含无论是否发生异常都要执行的代码。 finally
块通常用于清理在 try
块中分配的任何资源。 例如,确保变量具有分配给变量的正确值或必需值。
C# 应用程序中的异常处理通常使用以下一个或多个模式实现:
try-catch
模式包含一个try
块,后跟一个或多个catch
子句。 每个catch
块用于指定不同异常的处理程序。- 该
try-finally
模式由一个try
块组成,然后紧随一个finally
块。 通常,当控制离开try
语句时,finally
块的语句将运行。 - 该
try-catch-finally
模式实现所有三种类型的异常处理块。 模式中try-catch-finally
的常见场景是:在try
块中获取和使用资源,在catch
块中处理异常情况,并在finally
块中释放或管理资源。
如何在代码中表示异常?
异常在代码中表示为对象,这意味着它们是类的实例。 .NET 类库提供在代码中访问的异常类,就像其他 .NET 类一样。 用作代码中对象的 .NET 类的另一个示例是 Random
类(用于创建随机数)。
更确切地说,异常情况是由所有最终派生自 System.Exception
的类所表示的类型。 派生自 Exception
的异常类包括标识异常类型的信息,并包含提供有关异常的详细信息的属性。 本模块后面的部分将对 Exception
类进行更详细的探讨。
类的运行时实例通常称为对象,因此异常通常称为异常对象。
注释
虽然它们有时可以互换使用,但类和对象是不同的。 类定义对象类型,但它不是对象本身。 对象是基于类的具体实体。
异常处理过程
当发生异常时,.NET 运行时会查找最近的能够处理该异常的catch
子句。 该过程从导致引发异常的方法开始。 首先,检查该方法,以查看导致异常的代码是否在代码块内 try
。 如果代码位于 try
代码块内部,则按顺序考虑与 try
语句关联的 catch
子句。 若catch
子句无法处理异常,则会搜寻调用当前方法的方法。 通过检查此方法来确定对第一个方法的调用是否在try
代码块内。 如果调用位于 try
代码块内,则会考虑关联的 catch
子句。 此搜索过程将继续执行,直到找到一个可以处理当前异常的 catch
子句。
找到可以处理异常的 catch
子句后,运行时会准备将控制权转移到 catch
块的第一个语句。 但是,在开始执行 catch
块之前,运行时将执行与搜索期间找到的 try
语句关联的任何 finally
块。 如果找到多个 finally
块,则按顺序执行它们,从最接近导致引发异常的代码开始。
catch
如果未找到任何子句来处理异常,运行时将终止应用程序并向用户显示错误消息。
请考虑以下代码示例,其中包含 try-finally
模式嵌套在 try-catch
模式内:
try
{
// Step 1: code execution begins
try
{
// Step 2: an exception occurs here
}
finally
{
// Step 4: the system executes the finally code block associated with the try statement where the exception occurred
}
}
catch // Step 3: the system finds a catch clause that can handle the exception
{
// Step 5: the system transfers control to the first line of the catch code block
}
在此示例中,将发生以下过程:
- 执行过程从外部
try
语句的代码块开始。 - 在内部
try
语句的代码块中引发异常。 - 运行时查找与外部
try
语句关联的catch
子句。 - 在运行时将控制权转移到
catch
代码块的第一行之前,它会执行与内部try
语句关联的finally
子句。 - 然后,运行时将控制权传输到代码块的第一行
catch
,并执行处理异常的代码。
在此简单示例中,嵌套 try-catch
模式和 try-finally
模式驻留在单个方法中,但多个 try-catch
模式 try-finally
可以分布在调用其他方法的方法之间。
异常处理和调用堆栈
当你读到异常处理和异常处理过程时,通常会看到“调用堆栈展开”这个术语。 若要了解此术语,需要了解调用堆栈以及如何在代码执行期间跟踪方法调用的“堆栈”。
可以将调用堆栈想象成一座由很多块组成的塔。 当你建造一座塔时,你从一个块开始。 每次向塔添加一个块时,都会将它放在现有块的上面。 当应用程序在调试器中开始运行时,应用程序的入口点是添加到调用堆栈(塔的第一个块)的第一层。 每次方法调用另一个方法时,新方法都会添加到堆栈的顶部。 当代码从一个方法中退出时,该方法将从调用堆栈中删除。
注释
对于控制台应用程序,应用程序的入口点是顶层语句。 在 Visual Studio Code 调用堆栈中,此入口点称为 Main
该方法。
调用堆栈展开是 .NET 运行时在 C# 程序遇到错误时使用的过程。 这也是你刚查看的过程。
回到积木塔的比喻,当你需要从塔中取出一个积木块时,从顶部开始,依次取出每个积木块,直到取出所需的那块积木。 此过程类似于调用堆栈展开的工作过程,其中堆栈中的每个调用层就像塔中的一个块。 当运行时需要展开调用堆栈时,它会从顶部开始删除每个调用层,直到到达所需的那个调用层。 在本例中,它所需的调用层是具有 catch
子句的方法,该子句可以处理发生的异常。
回顾
在本单元中,应谨记以下几个重要事项:
- 可能需要异常处理的常见方案包括用户输入、数据处理、文件 I/O作、数据库作和网络通信。
- C# 中的异常处理是使用
try
和catch
finally
关键字实现的。 每个关键字都有一个关联的代码块,该块用于特定目的。 - 异常表示为类型,派生自
System.Exception
.NET 中的类。 异常包含标识异常类型的信息,以及提供其他详细信息的属性。 - 当发生异常时,.NET 运行时会搜索最近的可处理该异常的
catch
子句。 搜索从引发异常的方法开始,并在必要时向下移动调用堆栈。