异常处理语句 -
使用 throw 和 try 语句来处理异常。 使用 throw 语句引发异常。 使用 try 语句捕获和处理在执行代码块期间可能发生的异常。
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; 更新 StackTrace 的 e 属性。
引发异常时,公共语言运行时 (CLR) 将查找可以处理此异常的 catch 块。 如果当前执行的方法不包含此类 catch 块,则 CLR 查看调用了当前方法的方法,并以此类推遍历调用堆栈。 如果未找到 catch 块,CLR 将终止正在执行的线程。 有关详细信息,请参阅 C# 语言规范的如何处理异常部分。
throw 表达式
还可以将 throw 用作表达式。 这在很多情况下可能很方便,包括:
条件运算符。 以下示例使用
throw表达式在传递的数组 ArgumentException 为空时引发args:string first = args.Length >= 1 ? args[0] : throw new ArgumentException("Please supply at least one argument.");null 合并运算符。 以下示例使用
throw表达式在要分配给属性的字符串为 ArgumentNullException 时引发null:public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }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; 更新 StackTrace 的 e 属性。
when 异常筛选器
除了异常类型之外,还可以指定异常筛选器,该筛选器进一步检查异常并确定相应的 catch 块是否处理该异常。 异常筛选器是遵循 when 关键字的布尔表达式,如以下示例所示:
try
{
var result = Process(-3, 4);
Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
前面的示例使用异常筛选器提供单个 catch 块来处理两个指定类型的异常。
可以为相同异常类型提供若干 catch 子句,如果它们通过异常筛选器区分。 其中一个子句可能没有异常筛选器。 如果存在此类子句,则它必须是指定该异常类型的最后一个子句。
如果 catch 子句具有异常筛选器,则可以指定与 catch 子句之后出现的异常类型相同或小于派生的异常类型。 例如,如果存在异常筛选器,则 catch (Exception e) 子句不需要是最后一个子句。
异常筛选器与传统异常处理
异常筛选器比传统异常处理方法具有显著优势。 主要 区别在于评估 异常处理逻辑:
-
异常筛选器 (
when):在取消部署堆栈 之前 计算筛选器表达式。 这意味着原始调用堆栈和所有局部变量在筛选器计算期间保持不变。 -
传统
catch块:捕获块在堆栈解开 后 执行,可能会丢失有价值的调试信息。
下面是显示差异的比较:
public static void DemonstrateStackUnwindingDifference()
{
var localVariable = "Important debugging info";
try
{
ProcessWithExceptionFilter(localVariable);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
{
// Exception filter: Stack not unwound yet.
// localVariable is still accessible in debugger.
// Call stack shows original throwing location.
Console.WriteLine($"Caught with filter: {ex.Message}");
Console.WriteLine($"Local variable accessible: {localVariable}");
}
try
{
ProcessWithTraditionalCatch(localVariable);
}
catch (InvalidOperationException ex)
{
// Traditional catch: Stack already unwound.
// Some debugging information may be lost.
if (ex.Message.Contains("traditional"))
{
Console.WriteLine($"Caught with if: {ex.Message}");
Console.WriteLine($"Local variable accessible: {localVariable}");
}
else
{
throw; // Re-throws and further modifies stack trace.
}
}
}
private static void ProcessWithExceptionFilter(string context)
{
throw new InvalidOperationException($"Exception for filter demo: {context}");
}
private static void ProcessWithTraditionalCatch(string context)
{
throw new InvalidOperationException($"Exception for traditional demo: {context}");
}
异常筛选器的优点
- 更好的调试体验:由于堆栈在筛选器匹配之前不会解开,因此调试器可以显示原始故障点,且所有局部变量保持不变。
- 性能优势:如果没有筛选器匹配,则异常将继续传播,而无需堆栈展开和还原的开销。
- 更简洁的代码:多个筛选器可以处理相同异常类型的不同条件,而无需嵌套 if-else 语句。
- 日志记录和诊断:在决定是否处理异常之前,可以检查和记录异常详细信息:
public static void DemonstrateDebuggingAdvantage()
{
var contextData = new Dictionary<string, object>
{
["RequestId"] = Guid.NewGuid(),
["UserId"] = "user123",
["Timestamp"] = DateTime.Now
};
try
{
// Simulate a deep call stack.
Level1Method(contextData);
}
catch (Exception ex) when (LogAndFilter(ex, contextData))
{
// This catch block may never execute if LogAndFilter returns false.
// But LogAndFilter can examine the exception while the stack is intact.
Console.WriteLine("Exception handled after logging");
}
}
private static void Level1Method(Dictionary<string, object> context)
{
Level2Method(context);
}
private static void Level2Method(Dictionary<string, object> context)
{
Level3Method(context);
}
private static void Level3Method(Dictionary<string, object> context)
{
throw new InvalidOperationException("Error in deep call stack");
}
private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
// This method runs before stack unwinding.
// Full call stack and local variables are still available.
Console.WriteLine($"Exception occurred: {ex.Message}");
Console.WriteLine($"Request ID: {context["RequestId"]}");
Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
// Return true to handle the exception, false to continue search.
return ex.Message.Contains("deep call stack");
}
何时使用异常筛选器
需要以下条件时使用异常筛选器:
- 根据特定条件或属性处理异常。
- 保留原始调用堆栈以供调试。
- 在决定是否处理异常之前记录或检查异常。
- 根据上下文以不同的方式处理相同的异常类型。
public static void HandleFileOperations(string filePath)
{
try
{
// Simulate file operation that might fail.
ProcessFile(filePath);
}
catch (IOException ex) when (ex.Message.Contains("access denied"))
{
Console.WriteLine("File access denied. Check permissions.");
}
catch (IOException ex) when (ex.Message.Contains("not found"))
{
Console.WriteLine("File not found. Verify the path.");
}
catch (IOException ex) when (IsNetworkPath(filePath))
{
Console.WriteLine($"Network file operation failed: {ex.Message}");
}
catch (IOException)
{
Console.WriteLine("Other I/O error occurred.");
}
}
private static void ProcessFile(string filePath)
{
// Simulate different types of file exceptions.
if (filePath.Contains("denied"))
throw new IOException("File access denied");
if (filePath.Contains("missing"))
throw new IOException("File not found");
if (IsNetworkPath(filePath))
throw new IOException("Network timeout occurred");
}
private static bool IsNetworkPath(string path)
{
return path.StartsWith(@"\\") || path.StartsWith("http");
}
堆栈跟踪保留
异常筛选器保留原始 ex.StackTrace 属性。 如果子 catch 句无法处理异常并重新引发,原始堆栈信息将丢失。 筛选器 when 不会展开堆栈,因此,如果 when 筛选器为 false筛选器,则原始堆栈跟踪不会更改。
异常筛选器方法在保留调试信息对于诊断问题至关重要的应用程序中非常有用。
异步和迭代器方法中的异常
如果异步函数中发生异常,则等待函数的结果时,它会传播到函数的调用方,如以下示例所示:
public static async Task Run()
{
try
{
Task<int> processing = ProcessAsync(-1);
Console.WriteLine("Launched processing.");
int result = await processing;
Console.WriteLine($"Result: {result}.");
}
catch (ArgumentException e)
{
Console.WriteLine($"Processing failed: {e.Message}");
}
// Output:
// Launched processing.
// Processing failed: Input must be non-negative. (Parameter 'input')
}
private static async Task<int> ProcessAsync(int input)
{
if (input < 0)
{
throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
}
await Task.Delay(500);
return input;
}
如果迭代器方法中发生异常,则仅当迭代器前进到下一个元素时,它才会传播到调用方。
try-finally 语句
在 try-finally 语句中,当控件离开 finally 块时,将执行 try 块。 控件可能会离开 try 块,因为
- 正常执行,
- 执行 jump 语句(即
return、break、continue或goto),或 - 从
try块中传播异常。
以下示例使用 finally 块在控件离开方法之前重置对象的状态:
public async Task HandleRequest(int itemId, CancellationToken ct)
{
Busy = true;
try
{
await ProcessAsync(itemId, ct);
}
finally
{
Busy = false;
}
}
还可以使用 finally 块来清理 try 块中使用的已分配资源。
注意
当资源类型实现 IDisposable 或 IAsyncDisposable 接口时,请考虑 using 语句。
using 语句可确保在控件离开 using 语句时释放获取的资源。 编译器将 using 语句转换为 try-finally 语句。
finally 块的执行取决于操作系统是否选择触发异常解除操作。 未执行 finally 块的唯一情况涉及立即终止程序。 例如,由于 Environment.FailFast 调用或 OverflowException 或 InvalidProgramException 异常,可能会发生此类终止。 大多数操作系统在停止和卸载进程的过程中执行合理的资源清理。
try-catch-finally 语句
使用 try-catch-finally 语句来处理在执行 try 块期间可能发生的异常,并指定在控件离开 try 语句时必须执行的代码:
public async Task ProcessRequest(int itemId, CancellationToken ct)
{
Busy = true;
try
{
await ProcessAsync(itemId, ct);
}
catch (Exception e) when (e is not OperationCanceledException)
{
LogError(e, $"Failed to process request for item ID {itemId}.");
throw;
}
finally
{
Busy = false;
}
}
当 catch 块处理异常时,finally 块在执行该 catch 块后执行(即使执行 catch 块期间发生另一个异常)。 有关 catch 和 finally 块的信息,请分别参阅 try-catch 语句和 try-finally 语句 部分。
C# 语言规范
有关更多信息,请参阅 C# 语言规范的以下部分: