Skip to content

C# - 异常处理

异常是指程序执行期间发生的错误条件或意外事件。在 C# 中,当库或应用程序代码遇到异常情况(例如尝试除以零或访问不存在的文件)时,会创建并“抛出”一个异常对象。

异常提供了一种结构化、健壮且类型安全的方式来处理错误,将错误处理代码与主程序逻辑分离。

C# 提供了一个强大的异常处理机制,它基于以下四个主要关键字:

  • try:try 块包含可能抛出异常的代码。它是代码中的“受保护”区域。
  • catch:catch 块是异常处理器。当在相应的 try 块中抛出特定类型的异常时,它会执行。可以有多个 catch 块来处理不同类型的异常。
  • finally:finally 块包含的代码总是会执行,无论是否抛出或捕获了异常。它非常适合用于释放文件句柄或数据库连接等资源。
  • throw:throw 关键字用于发出异常发生的信号。你可以抛出一个新的异常,或者从 catch 块中重新抛出(re-throw)一个已有的异常。

异常处理的基本结构包括一个 try 块,后跟一个或多个 catch 块,以及可选的 finally 块。

try
{
// 可能导致异常的代码。
// 例如,文件操作或网络请求。
}
catch (SpecificExceptionType ex1)
{
// 处理特定异常的代码。
// 记录错误,通知用户等。
}
catch (AnotherExceptionType ex2) when (ex2.SomeCondition)
{
// 使用异常过滤器(when 子句)进行条件捕获。
// 这对于更细粒度的错误处理很有用。
}
catch (Exception ex)
{
// 用于捕获其他任何异常的通用 catch 块。
// 最佳实践是先捕获特定异常。
}
finally
{
// 总是会执行的代码。
// 非常适合清理任务,例如关闭文件。
}

现代 C# 还提供了 using 声明,它简化了资源管理,通常可以替代显式 finally 块来释放资源。

在 .NET 中,所有异常都派生自基类 System.Exception。此基类提供了常见的属性,例如 Message(错误的描述)和 StackTrace(有关错误发生位置的信息)。

从 System.Exception 派生出来的内置异常主要有两个分支:

  • System.SystemException:由 .NET 运行时(CLR)本身抛出的异常的基类。示例包括 NullReferenceException 和 IndexOutOfRangeException。
  • System.ApplicationException:该类历史上曾被设计为用户定义异常的基类。然而,现代最佳实践是直接从 System.Exception 派生自定义异常。

以下是一些常见的内置异常类:

Exception ClassDescription
System.IO.IOException与 I/O 相关的错误的基类,例如 FileNotFoundException。
System.IndexOutOfRangeException当尝试使用无效索引访问数组或集合时抛出。
System.NullReferenceException当尝试访问空(null)对象的成员时抛出。
System.DivideByZeroException在整数除以零时抛出。
System.InvalidCastException当显式类型转换失败时抛出。
System.OutOfMemoryException当应用程序可用内存不足时抛出。
System.ArgumentException与无效方法参数相关的异常的基类,例如 ArgumentNullException。

此示例演示了如何处理 DivideByZeroException。即使捕获到异常,finally 块也会执行。

// 此示例可以使用现代 C# 中的顶级语句运行。
int SafeDivide(int numerator, int denominator)
{
int result = 0;
try
{
Console.WriteLine("Attempting to divide...");
result = numerator / denominator;
Console.WriteLine("Division successful.");
}
catch (DivideByZeroException ex)
{
// 使用字符串内插以获得更清晰的输出。
Console.WriteLine($"Error: Cannot divide by zero. Details: {ex.Message}");
}
finally
{
Console.WriteLine("The 'finally' block is always executed.");
Console.WriteLine($"Final result variable is: {result}");
}
return result;
}
SafeDivide(25, 5); // Successful case
Console.WriteLine("---_---");
SafeDivide(25, 0); // Exception case
/*
* 预期输出:
*
* Attempting to divide...
* Division successful.
* The 'finally' block is always executed.
* Final result variable is: 5
* ---_---
* Attempting to divide...
* Error: Cannot divide by zero. Details: Attempted to divide by zero.
* The 'finally' block is always executed.
* Final result variable is: 0
*/

创建自己的异常类型是表示应用程序领域中特定错误条件的好方法。最佳实践是直接从 System.Exception 派生,并遵循以 Exception 结尾的类名命名约定。

示例 2:自定义 InvalidTemperatureException

Section titled “示例 2:自定义 InvalidTemperatureException”
// 用于特定领域错误的自定义异常。
public class InvalidTemperatureException : Exception
{
public InvalidTemperatureException(string message) : base(message) { }
}
public class Thermostat
{
public void SetTemperature(int tempInCelsius)
{
// 绝对零度是 -273.15°C。
if (tempInCelsius < -273)
{
throw new InvalidTemperatureException($"{tempInCelsius}°C is below absolute zero!");
}
Console.WriteLine($"Temperature set to {tempInCelsius}°C");
}
}
// 主执行逻辑
var thermostat = new Thermostat();
try
{
thermostat.SetTemperature(25);
thermostat.SetTemperature(-300);
}
catch (InvalidTemperatureException ex)
{
Console.WriteLine($"Caught a custom domain error: {ex.Message}");
}
/*
* 预期输出:
*
* Temperature set to 25°C
* Caught a custom domain error: -300°C is below absolute zero!
*/

有时你需要捕获一个异常,执行一些操作(例如日志记录),然后让异常继续沿着调用堆栈向上冒泡。为此,关键是使用 throw;,而不是 throw ex;。

  • throw;:重新抛出原始异常,保留其原始堆栈跟踪。这是重新抛出的正确方式。
  • throw ex;:抛出一个新异常,将堆栈跟踪重置为当前位置。这会隐藏错误的原始来源,应避免使用,除非你是有意包装异常。
void ProcessData()
{
try
{
// 模拟一个底层操作
throw new InvalidOperationException("Data corruption detected.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine("Logging error at low level...");
// 正确地重新抛出,以便更高级别的处理器可以处理。
throw;
}
}
try
{
ProcessData();
}
catch (Exception ex)
{
Console.WriteLine("在顶层捕获。请参阅下面的完整堆栈跟踪:");
Console.WriteLine(ex.StackTrace);
}
  • 捕获特定异常:除非你处于应用程序的顶层(例如,在全局错误处理器中),否则应避免捕获通用 System.Exception。捕获特定异常可以使你的代码更可预测、更健壮。
  • 对 IDisposable 使用 using:对于实现 IDisposable 的对象(例如 FileStream 或 HttpClient),首选 using 语句或声明,以确保资源始终被释放。编译器会为你将其转换为 try/finally 块。
  • 不要将异常用于控制流:异常用于异常情况,而不是正常的程序逻辑。它们速度慢,并且可能使代码难以理解。对于预期情况,请使用 if 语句或 int.TryParse 等方法。
  • 包含 InnerException:当你捕获一个异常并将其包装到一个新的、更具体的异常中时,将原始异常传递给新异常的构造函数。这会保留完整的错误链。
  • 吞噬异常:一个什么都不做的 catch 块(catch (Exception) {})是危险的。它会隐藏问题,使调试几乎不可能。
  • 使用 throw ex 捕获并重新抛出:如前所述,这会破坏原始堆栈跟踪。始终使用 throw;。
  • 过度使用自定义异常:仅当自定义异常表示应用程序领域中独特、可操作的错误时才创建它们。不要为每个可能的错误都创建一个新的异常类型。