目录
C# 异常处理
C # 中的异常提供了结构化、统一和类型安全的方式来处理系统级别和应用程序级别的错误条件。 C # 中的异常机制与 c + + 中的异常机制非常相似,但有几个重要的区别:
- 在 c # 中,所有异常必须由派生自的类类型的实例表示 System.Exception 。 在 c + + 中,任何类型的任何值都可以用来表示异常。
- 在 c # 中,可以使用 try 语句 (finally 块) 来编写在正常执行和异常情况下执行的终止代码。 此类代码难以用 c + + 编写,无需复制代码。
- 在 c # 中,系统级别的异常(如溢出、被零除和空引用)具有定义完善的异常类,并且与应用程序级别的错误条件是同等的。
异常的原因
可以通过两种不同的方法引发异常。
- throw Throw 语句 (的语句) 会立即、无条件地引发异常。 控件永远不会到达紧跟的语句后面的语句 throw 。
- 在处理 c # 语句和表达式过程中出现的某些异常情况会在某些情况下导致无法正常完成操作时引发异常。 例如,如果分母为零,则 (除法运算符) 整数除法运算将引发 System.DivideByZeroException 。
System.Exception 类
System.Exception类是所有异常的基类型。 此类具有一些例外的属性,这些属性是所有异常都共享的:
- Message 是类型的只读属性 string ,它包含对异常原因的可读说明。
- InnerException 是类型的只读属性 Exception 。 如果其值不为 null,则表示导致当前异常的异常,即在处理的 catch 块中引发当前异常 InnerException 。 否则,其值为 null,表示此异常不是由其他异常导致的。 以这种方式链接在一起的异常对象的数量可能是任意的。
可以在对的实例构造函数的调用中指定这些属性的值 System.Exception 。
如何处理异常
异常由 try语句处理。
发生异常时,系统会搜索 catch 可处理异常的最近子句,如异常的运行时类型所确定。 首先,搜索当前方法,查找词法上的封闭 try 语句,并按顺序考虑 try 语句的关联的 catch 子句。 如果此操作失败,则会在调用当前方法的方法中搜索一个词法封闭 try 语句,该语句将调用的点包围到当前方法。 此搜索将继续执行 catch ,直到找到可处理当前异常的子句为止,方法是将与所引发的异常的运行时类型的异常类命名为相同的类或基类。 catch不命名异常类的子句可处理任何异常。
找到匹配的 catch 子句后,系统准备将控制转移到 catch 子句的第一条语句。 在开始执行 catch 子句之前,系统首先会按顺序执行 finally 与尝试语句相关联的、与捕获异常的子句更嵌套的所有子句。
如果未找到匹配的 catch 子句,则会出现以下两种情况之一:
- 如果在 (静态构造函数) 或静态字段初始值设定项的情况下搜索匹配的 catch 子句,则 System.TypeInitializationException 会在触发静态构造函数调用的那一点引发。 的内部异常 System.TypeInitializationException 包含最初引发的异常。
- 如果对匹配的 catch 子句的搜索到达最初启动线程的代码,则终止线程的执行。 此类终止的影响是由实现定义的。
在析构函数执行过程中发生的异常值得特别提。 如果在析构函数执行过程中发生异常,且未捕获该异常,则将终止该析构函数的执行,并且如果调用了任何) ,则将 (基类的析构函数。 如果在类型) 的情况下没有 (的基类 object ,或者如果没有基类析构函数,则放弃该异常。
常见的异常类
异常类 | 描述 |
---|---|
System.IO.IOException | 处理 I/O 错误。 |
System.IndexOutOfRangeException | 处理当方法指向超出范围的数组索引时生成的错误。 |
System.ArrayTypeMismatchException | 处理当数组类型不匹配时生成的错误。 |
System.NullReferenceException | 处理当依从一个空对象时生成的错误。 |
System.DivideByZeroException | 处理当除以零时生成的错误。 |
System.InvalidCastException | 处理在类型转换期间生成的错误。 |
System.OutOfMemoryException | 处理空闲内存不足生成的错误。 |
System.StackOverflowException | 处理栈溢出生成的错误。 |
throw 语句
throw 语句引发异常:
if (shapeAmount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}
在 throw e; 语句中,表达式 e 的结果必须隐式转换为 System.Exception。
可以使用内置异常类,例如 ArgumentOutOfRangeException 或 InvalidOperationException。 .NET 还提供了在某些情况下引发异常的帮助程序方法:ArgumentNullException.ThrowIfNull 和 ArgumentException.ThrowIfNullOrEmpty。 还可以定义自己的派生自 System.Exception 的异常类。 有关详细信息,请参阅创建和引发异常。
在 catch 块内,可以使用 throw; 语句重新引发由 catch 块处理的异常:
try
{
ProcessShapes(shapeAmount);
}
catch (Exception e)
{
LogError(e, "Shape processing failed.");
throw;
}
注意:throw;
保留异常的原始堆栈跟踪,该跟踪存储在 Exception.StackTrace 属性中。 与此相反,throw e;
更新 e
的 StackTrace 属性。
引发异常时,公共语言运行时 (CLR) 将查找可以处理此异常的 catch 块。 如果当前执行的方法不包含此类 catch 块,则 CLR 查看调用了当前方法的方法,并以此类推遍历调用堆栈。 如果未找到 catch 块,CLR 将终止正在执行的线程。 有关详细信息,请参阅 C# 语言规范的如何处理异常部分。
以下是一个简单的例子,演示了如何使用throw语句抛出一个自定义的异常:
using System;
namespace ExceptionHandling
{
class Program
{
static void Main(string[] args)
{
try
{
int divisor = 0;
if (divisor == 0)
{
throw new DivideByZeroException("除数不能为零!");
}
else
{
int result = 10 / divisor;
Console.WriteLine("结果: " + result);
}
}
catch (DivideByZeroException e)
{
Console.WriteLine("捕获到异常: " + e.Message);
}
Console.ReadKey();
}
}
}
在这个示例中,当divisor的值为0时,我们手动抛出了一个DivideByZeroException异常。在catch块中,我们捕获这个异常并输出异常信息。
throw 表达式
还可以将 throw 用作表达式。 这在很多情况下可能很方便,包括:
1、条件运算符。以下示例使用 throw 表达式在传递的数组 args 为空时引发ArgumentException:
string first = args.Length >= 1
? args[0]
: throw new ArgumentException("Please supply at least one argument.");
2、null 合并运算符。 以下示例使用 throw 表达式在要分配给属性的字符串为 null 时引发ArgumentNullException:
public string Name
{
get => name;
set => name = value ??
throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}
3、expression-bodied lambda 或方法。 以下示例使用 throw
表达式引发 InvalidCastException,以指示不支持转换为 DateTime 值:
DateTime ToDateTime(IFormatProvider provider) =>
throw new InvalidCastException("Conversion to a DateTime is not supported.");
try 语句
可以通过以下任何形式使用 try 语句:try-catch - 处理在 try 块内执行代码期间可能发生的异常,try-finally - 指定在控件离开 try 块时执行的代码,以及 try-catch-finally - 作为上述两种形式的组合。
try-catch 语句
使用 try-catch 语句处理在执行代码块期间可能发生的异常。 将代码置于 try 块中可能发生异常的位置。 使用 catch 子句指定要在相应的 catch 块中处理的异常的基类型:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
可以提供多个 catch 子句:
try
{
var result = await ProcessAsync(-3, 4, cancellationToken);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Processing is cancelled.");
}
发生异常时,将从上到下按指定顺序检查 catch 子句。 对于任何引发的异常,最多只执行一个 catch 块。 如前面的示例所示,可以省略异常变量的声明,并在 catch 子句中仅指定异常类型。 没有任何指定异常类型的 catch 子句与任何异常匹配,如果存在,则必须是最后一个 catch 子句。
如果要重新引发捕获的异常,请使用 throw 语句,如以下示例所示:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
LogError(e, "Processing failed.");
throw;
}
注意:throw;
保留异常的原始堆栈跟踪,该跟踪存储在 Exception.StackTrace 属性中。 与此相反,throw e;
更新 e
的 StackTrace 属性。
try-finally 语句
在C#中,try-finally语句用于确保无论是否发生异常,某些代码块都会得到执行。try块中的代码用于包含可能引发异常的逻辑,而finally块中的代码则始终会在try块中的代码执行完毕后被执行,无论是否发生了异常。
下面是一个简单的示例,演示了try-finally语句的用法:
using System;
class Program
{
static void Main()
{
try
{
Console.WriteLine("Try块:执行一些可能引发异常的操作。");
throw new Exception("出了问题");
}
finally
{
Console.WriteLine("最后一块:清理和释放资源。");
}
}
}
在这个示例中,无论try块中抛出了异常,finally块中的代码总是会得到执行。这使得finally块非常适合用于资源释放、清理或者确保某些必要的操作得到执行。
需要注意的是,如果在try块中抛出了异常,并且该异常在catch块中得到处理,finally块仍然会在catch块执行完毕后被执行。
注意:当资源类型实现 IDisposable 或 IAsyncDisposable 接口时,请考虑 using 语句。 using
语句可确保在控件离开 using
语句时释放获取的资源。 编译器将 using
语句转换为 try-finally
语句。
在几乎所有情况下,都会执行 finally
块。 未执行 finally
块的唯一情况涉及立即终止程序。 例如,由于 Environment.FailFast 调用或 OverflowException 或 InvalidProgramException 异常,可能会发生此类终止。 大多数操作系统在停止和卸载进程的过程中执行合理的资源清理。
try-catch-finally 语句
在C#中,try-catch-finally语句用于处理可能引发异常的代码块,并提供对异常的处理逻辑和资源清理的机会。
try块用于包含可能引发异常的代码,如果在try块中抛出了异常,程序会跳转到匹配的catch块进行异常处理。catch块可以捕获特定类型的异常并执行相应的处理逻辑。一个try块可以有多个catch块,每个catch块可以处理不同类型的异常。
finally块用于包含在try块中抛出异常或正常执行完成后都必须执行的代码。finally块中的代码总是会在try块的代码执行完毕后被执行,无论是否发生了异常。
以下是一个示例,演示了try-catch-finally语句的用法:
using System;
class Program
{
static void Main()
{
try
{
Console.WriteLine("Try块:执行一些可能引发异常的操作。");
throw new Exception("出了问题");
}
catch (Exception e)
{
Console.WriteLine($"Catch块:捕获到异常- {e.Message}");
}
finally
{
Console.WriteLine("最后一块:清理和释放资源。");
}
}
}
在这个示例中,我们在try块中抛出了一个异常,然后在catch块中捕获并处理该异常。无论是否发生了异常,finally块中的代码都会得到执行,用于进行资源清理等操作。
需要注意的是,catch块是可选的,你可以只使用try-finally语句来进行资源清理,而不处理异常。
when 异常筛选器
除了异常类型之外,还可以指定异常筛选器,该筛选器进一步检查异常并确定相应的 catch 块是否处理该异常。 异常筛选器是遵循 when 关键字的布尔表达式,如以下示例所示:
using System;
namespace ExceptionHandling
{
class Program
{
static void Main(string[] args)
{
try
{
int[] numbers = { 1, 2 };
Console.WriteLine(numbers[5]); // 这里会抛出 IndexOutOfRangeException
}
catch (Exception e) when (e is IndexOutOfRangeException)
{
Console.WriteLine("捕获到数组越界异常,并且满足附加条件!");
}
catch (Exception e)
{
Console.WriteLine("捕获到其他异常:" + e.Message);
}
Console.ReadKey();
}
}
}
在这个示例中,当访问数组越界时会抛出IndexOutOfRangeException异常。在第一个catch块中,我们使用when关键字来附加一个条件,即只有当捕获到的异常是IndexOutOfRangeException并且满足附加条件时,才会执行这个catch块。
注意:
可以为相同异常类型提供若干 catch 子句,如果它们通过异常筛选器区分。 其中一个子句可能没有异常筛选器。 如果存在此类子句,则它必须是指定该异常类型的最后一个子句。
如果 catch 子句具有异常筛选器,则可以指定与 catch 子句之后出现的异常类型相同或小于派生的异常类型。 例如,如果存在异常筛选器,则 catch (Exception e) 子句不需要是最后一个子句。
异步和迭代器方法中的异常
如果异步函数中发生异常,则等待函数的结果时,它会传播到函数的调用方,如以下示例所示:
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
try
{
foreach (var item in GenerateSequence())
{
Console.WriteLine(item);
}
}
catch (Exception ex)
{
Console.WriteLine("捕获到异常:" + ex.Message);
}
Console.ReadKey();
}
static IEnumerable<int> GenerateSequence()
{
yield return 1;
yield return 2;
throw new Exception("发生了异常!");
yield return 3; // 永远不会执行到这里
}
}
在这个示例中,GenerateSequence()是一个迭代器方法,它生成一个整数序列。在迭代过程中,我们抛出了一个异常。当迭代器前进到下一个元素时,异常被传播到调用方。在Main()方法中,我们使用foreach循环迭代序列,并使用try-catch块捕获异常。
请注意,当异常被捕获后,迭代器的后续元素将不会被生成,因为迭代器方法已经退出。