C# - 异常处理
C# - 现代异常处理
Section titled “C# - 现代异常处理”什么是异常?
Section titled “什么是异常?”异常是指程序执行期间发生的错误条件或意外事件。在 C# 中,当库或应用程序代码遇到异常情况(例如尝试除以零或访问不存在的文件)时,会创建并“抛出”一个异常对象。
异常提供了一种结构化、健壮且类型安全的方式来处理错误,将错误处理代码与主程序逻辑分离。
异常处理的核心关键字
Section titled “异常处理的核心关键字”C# 提供了一个强大的异常处理机制,它基于以下四个主要关键字:
- try:
try块包含可能抛出异常的代码。它是代码中的“受保护”区域。 - catch:
catch块是异常处理器。当在相应的try块中抛出特定类型的异常时,它会执行。可以有多个catch块来处理不同类型的异常。 - finally:
finally块包含的代码总是会执行,无论是否抛出或捕获了异常。它非常适合用于释放文件句柄或数据库连接等资源。 - throw:
throw关键字用于发出异常发生的信号。你可以抛出一个新的异常,或者从catch块中重新抛出(re-throw)一个已有的异常。
异常处理结构
Section titled “异常处理结构”异常处理的基本结构包括一个 try 块,后跟一个或多个 catch 块,以及可选的 finally 块。
try{ // 可能导致异常的代码。 // 例如,文件操作或网络请求。}catch (SpecificExceptionType ex1){ // 处理特定异常的代码。 // 记录错误,通知用户等。}catch (AnotherExceptionType ex2) when (ex2.SomeCondition){ // 使用异常过滤器(when 子句)进行条件捕获。 // 这对于更细粒度的错误处理很有用。}catch (Exception ex){ // 用于捕获其他任何异常的通用 catch 块。 // 最佳实践是先捕获特定异常。}finally{ // 总是会执行的代码。 // 非常适合清理任务,例如关闭文件。}现代 C# 还提供了 using 声明,它简化了资源管理,通常可以替代显式 finally 块来释放资源。
.NET 中的异常层次结构
Section titled “.NET 中的异常层次结构”在 .NET 中,所有异常都派生自基类 System.Exception。此基类提供了常见的属性,例如 Message(错误的描述)和 StackTrace(有关错误发生位置的信息)。
从 System.Exception 派生出来的内置异常主要有两个分支:
- System.SystemException:由 .NET 运行时(CLR)本身抛出的异常的基类。示例包括
NullReferenceException和IndexOutOfRangeException。 - System.ApplicationException:该类历史上曾被设计为用户定义异常的基类。然而,现代最佳实践是直接从
System.Exception派生自定义异常。
以下是一些常见的内置异常类:
| Exception Class | Description |
|---|---|
System.IO.IOException | 与 I/O 相关的错误的基类,例如 FileNotFoundException。 |
System.IndexOutOfRangeException | 当尝试使用无效索引访问数组或集合时抛出。 |
System.NullReferenceException | 当尝试访问空(null)对象的成员时抛出。 |
System.DivideByZeroException | 在整数除以零时抛出。 |
System.InvalidCastException | 当显式类型转换失败时抛出。 |
System.OutOfMemoryException | 当应用程序可用内存不足时抛出。 |
System.ArgumentException | 与无效方法参数相关的异常的基类,例如 ArgumentNullException。 |
示例 1:基本的 try-catch-finally
Section titled “示例 1:基本的 try-catch-finally”此示例演示了如何处理 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 caseConsole.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*/创建自定义异常
Section titled “创建自定义异常”创建自己的异常类型是表示应用程序领域中特定错误条件的好方法。最佳实践是直接从 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!*/重新抛出异常
Section titled “重新抛出异常”有时你需要捕获一个异常,执行一些操作(例如日志记录),然后让异常继续沿着调用堆栈向上冒泡。为此,关键是使用 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);}最佳实践和常见错误
Section titled “最佳实践和常见错误”- 捕获特定异常:除非你处于应用程序的顶层(例如,在全局错误处理器中),否则应避免捕获通用
System.Exception。捕获特定异常可以使你的代码更可预测、更健壮。 - 对
IDisposable使用using:对于实现IDisposable的对象(例如FileStream或HttpClient),首选using语句或声明,以确保资源始终被释放。编译器会为你将其转换为try/finally块。 - 不要将异常用于控制流:异常用于异常情况,而不是正常的程序逻辑。它们速度慢,并且可能使代码难以理解。对于预期情况,请使用
if语句或int.TryParse等方法。 - 包含
InnerException:当你捕获一个异常并将其包装到一个新的、更具体的异常中时,将原始异常传递给新异常的构造函数。这会保留完整的错误链。
- 吞噬异常:一个什么都不做的
catch块(catch (Exception) {})是危险的。它会隐藏问题,使调试几乎不可能。 - 使用
throw ex捕获并重新抛出:如前所述,这会破坏原始堆栈跟踪。始终使用throw;。 - 过度使用自定义异常:仅当自定义异常表示应用程序领域中独特、可操作的错误时才创建它们。不要为每个可能的错误都创建一个新的异常类型。