C# 异常处理
异常处理是稳健应用程序开发的一个重要方面。 Tim Corey 的视频"Handling Exceptions in C# - When to catch them, where to catch them, and how to catch them"详细解释了什么是异常、如何处理异常以及在何处处理异常。
本文旨在使用 Tim Corey 的视频解释 C# 中的异常处理。 这是一项强大的功能,允许开发人员管理程序执行过程中出现的错误和特殊情况。 通过使用 try、catch 和 finally 块,C# 提供了一种结构化的方法来处理运行时错误、记录异常并保持程序流程。
简介
Tim 首先解释说,许多开发人员对异常及其处理的看法并不正确。 他强调了理解异常的重要性,以及如何正确处理异常以创建更强大的应用程序。
构建演示控制台应用程序
Tim 在 Visual Studio 2017 中创建了一个控制台应用程序来演示异常处理。 他建议在测试新主题时使用控制台应用程序,因为它们只需要最少的设置,而且易于操作。
using System;
namespace ExceptionsDemo
{
class Program
{
static void Main(string[] args)
{
// Placeholder for input and output operations
Console.ReadLine();
}
}
}using System;
namespace ExceptionsDemo
{
class Program
{
static void Main(string[] args)
{
// Placeholder for input and output operations
Console.ReadLine();
}
}
}创建类库
Tim 在解决方案中添加了一个类库,以模拟现实世界中不同方法相互调用的场景。

他删除了默认类,并创建了一个名为 DemoCode 的新类。
public class DemoCode
{
// Method to retrieve a number based on the provided position
public int GetNumber(int position)
{
int[] numbers = { 1, 4, 7, 2 };
return numbers[position];
}
// Intermediate method calls GetNumber
public int ParentMethod(int position)
{
return GetNumber(position);
}
// Top-level method calls ParentMethod
public int GrandparentMethod(int position)
{
return ParentMethod(position);
}
}public class DemoCode
{
// Method to retrieve a number based on the provided position
public int GetNumber(int position)
{
int[] numbers = { 1, 4, 7, 2 };
return numbers[position];
}
// Intermediate method calls GetNumber
public int ParentMethod(int position)
{
return GetNumber(position);
}
// Top-level method calls ParentMethod
public int GrandparentMethod(int position)
{
return ParentMethod(position);
}
}DemoCode 类包含相互调用的方法,最终根据给定的位置从数组中检索一个数字。
模拟异常
Tim 解释说,该应用程序旨在展示失败而非成功。 他通过向 GrandparentMethod 传递无效位置引入了越界异常。
DemoCode demo = new DemoCode();
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");DemoCode demo = new DemoCode();
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");在位置无效的情况下运行上述代码会导致 IndexOutOfRangeException 异常。 Tim 展示了 Visual Studio 调试器如何突出显示问题并提供有关异常的详细信息。
如何不使用 try-catch.
Tim 解释了开发人员在初次了解 try-catch 块时常犯的一个错误。 他们经常会在预期可能出现异常的地方封装整个代码块,这可能会导致处理不当。
try
{
int output = 0;
output = numbers[position];
return output;
}
catch (Exception ex)
{
// Avoid returning default values that can mask the problem
return 0;
}try
{
int output = 0;
output = numbers[position];
return output;
}
catch (Exception ex)
{
// Avoid returning default values that can mask the problem
return 0;
}Tim 强调指出,这种方法存在问题,因为它隐藏了例外情况,并以不正确的假设继续执行。 例如,将 0 作为默认值返回可能并不合适,而且会引起更多问题。
正确的异常处理
Tim 强调,异常提供了有关应用程序中意外状态的重要信息。 如果应用程序继续处于这种状态而不进行适当处理,可能会导致更多错误和数据损坏。
必须妥善处理异常情况,而不是一味吞并异常情况。 这里有一个更好的方法:
try
{
return numbers[position];
}
catch (Exception ex)
{
// Log the exception or handle it appropriately
Console.WriteLine(ex.Message);
throw; // Re-throw the exception to be handled by a higher-level handler
}try
{
return numbers[position];
}
catch (Exception ex)
{
// Log the exception or handle it appropriately
Console.WriteLine(ex.Message);
throw; // Re-throw the exception to be handled by a higher-level handler
}通过重新抛出异常,可以确保问题得到传播,并在必要时在更高层次进行处理。
为用户提供有用的信息
Tim 解释说,虽然可以优雅地处理某些异常而不会导致应用程序崩溃,但重要的是要向用户提供有用的反馈。 例如,显示带有重试操作选项的消息框或通知。
更多有用信息:堆栈跟踪
Tim 演示了如何使用异常对象的 StackTrace 属性来获取异常发生位置的详细信息。 这包括类、方法和行号,这对调试非常重要。
try
{
return numbers[position];
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
throw;
}try
{
return numbers[position];
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
throw;
}StackTrace 属性可提供完整的调用堆栈跟踪,帮助开发人员准确定位问题。
正确使用 try-catch.
Tim 解释说,正确处理异常不仅仅是捕获异常,还要知道在哪里放置 try-catch 块。 关键是要将 try-catch 块放在有足够的上下文来适当处理异常的级别。
位置不当示例
将 try-catch 块放在调用堆栈的深处通常无法有效处理异常,因为您缺乏高层操作的上下文。
// Deep level exception handling (not ideal)
try
{
return numbers[position];
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}// Deep level exception handling (not ideal)
try
{
return numbers[position];
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}正确位置示例
将 try-catch 块放在顶层,如用户界面或应用程序的入口点,可以让您根据操作的完整上下文来处理异常。
try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}这样,您就可以向用户提供更多信息,并决定应用程序是否可以继续运行或是否应该终止。
堆栈跟踪信息
Tim 强调了堆栈跟踪信息在诊断异常中的重要性。 堆栈跟踪提供了详细的调用历史,显示了异常发生的位置以及导致异常的方法调用链。
try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}该输出可提供异常的确切位置和代码路径,从而更容易调试和修复问题。
处理逻辑演示
Tim 演示了如何在适当的级别处理逻辑。 例如,如果一个方法负责打开和关闭数据库连接,它就应该处理异常,以确保资源得到妥善管理。
public int GrandparentMethod(int position)
{
try
{
Console.WriteLine("Open database connection");
int output = ParentMethod(position);
Console.WriteLine("Close database connection");
return output;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Ensure the exception is propagated
}
}public int GrandparentMethod(int position)
{
try
{
Console.WriteLine("Open database connection");
int output = ParentMethod(position);
Console.WriteLine("Close database connection");
return output;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Ensure the exception is propagated
}
}在此示例中,如果出现异常,数据库连接将无法正确关闭,从而导致潜在的资源泄漏。 通过添加 try-catch 块,可以确保即使出现异常也能关闭连接。
使用最终块
Tim 介绍了 finally 块,它可以确保某些代码无论是否出现异常都能运行。 这对于清理资源(如关闭数据库连接)尤其有用。
public int GrandparentMethod(int position)
{
try
{
Console.WriteLine("Open database connection");
int output = ParentMethod(position);
return output;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Re-throw the exception to ensure it's handled by a higher-level handler
}
finally
{
Console.WriteLine("Close database connection");
}
}public int GrandparentMethod(int position)
{
try
{
Console.WriteLine("Open database connection");
int output = ParentMethod(position);
return output;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Re-throw the exception to ensure it's handled by a higher-level handler
}
finally
{
Console.WriteLine("Close database connection");
}
}finally 块在 try 和 catch 块之后运行,确保即使出现异常也能关闭连接。
声明
Tim 解释了重新抛出异常以将其传递到调用栈的重要性。 这样上级处理程序就可以适当处理异常。
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Re-throws the exception to be handled by the calling method
}catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw; // Re-throws the exception to be handled by the calling method
}用 throw 重新抛出异常; 确保保留完整的堆栈跟踪,为调试提供有价值的上下文。
正确颠簸异常
Tim 演示了异常是如何在调用堆栈中冒泡的。 每个方法都会检查 try-catch 块,并处理异常或将其传递给调用者。
try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}try
{
int result = demo.GrandparentMethod(4); // This will cause an IndexOutOfRangeException
Console.WriteLine($"The value at the given position is {result}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}在本例中,GrandparentMethod 捕获异常、记录异常并重新抛出异常。 然后,控制台应用程序中的顶级 try-catch 块会处理异常并显示错误消息和堆栈跟踪。
异常处理中的常见错误
Tim 强调了开发人员在处理异常时常犯的几个错误:
1.Using throw ex;:
重写堆栈跟踪,丢失有价值的上下文。
- 举例说明:
catch (Exception ex) { // Incorrect throw ex; // Rewrites stack trace }catch (Exception ex) { // Incorrect throw ex; // Rewrites stack trace }
2.抛出新异常:
使用自定义消息创建新异常,但会丢失原始堆栈跟踪。
- 举例说明:
catch (Exception ex) { // Incorrect throw new Exception("I blew up"); }catch (Exception ex) { // Incorrect throw new Exception("I blew up"); }
在不丢失原始堆栈跟踪的情况下创建新异常
Tim 解释了如何在保留原始堆栈跟踪的同时创建新的异常。 当您想提供更有意义的错误信息或不同的异常类型,同时又想保留原始错误的上下文时,这将非常有用。
catch (Exception ex)
{
throw new ArgumentException("You passed in bad data", ex);
}catch (Exception ex)
{
throw new ArgumentException("You passed in bad data", ex);
}通过传递原始异常 (ex) 作为内部异常,可以保留原始堆栈跟踪,这对调试至关重要。
保留堆栈跟踪信息
Tim 演示了在创建新异常时如何访问原始异常消息和堆栈跟踪。
catch (Exception ex)
{
Console.WriteLine("You passed in bad data");
Console.WriteLine(ex.StackTrace);
throw new ArgumentException("You passed in bad data", ex);
}catch (Exception ex)
{
Console.WriteLine("You passed in bad data");
Console.WriteLine(ex.StackTrace);
throw new ArgumentException("You passed in bad data", ex);
}这样可以确保堆栈中抛出的异常包含新信息和原始异常详细信息。
通过内部异常循环
Tim 提供了一种在所有内部异常中循环以提取其消息和堆栈跟踪的方法。
catch (Exception ex)
{
Exception inner = ex;
while (inner != null)
{
Console.WriteLine(inner.StackTrace);
inner = inner.InnerException;
}
throw;
}catch (Exception ex)
{
Exception inner = ex;
while (inner != null)
{
Console.WriteLine(inner.StackTrace);
inner = inner.InnerException;
}
throw;
}该循环迭代每个内部异常,打印其堆栈跟踪,确保所有层级的异常都得到考虑。
以不同的方式处理不同的异常
Tim 讨论了如何使用多个捕获块处理不同类型的异常。 这样可以根据异常类型进行特定处理。
try
{
// Code that might throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}try
{
// Code that might throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}在此示例中,ArgumentException 通过打印自定义消息进行专门处理,而所有其他异常则返回到打印异常消息和堆栈跟踪的通用处理程序。

多个捕获块中顺序的重要性
Tim 强调了使用多个捕获块时顺序的重要性。 应首先捕捉最特殊的异常,然后再捕捉更一般的异常。
try
{
// Code that might throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}try
{
// Code that might throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine("You gave us bad information. Bad user!");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}如果在特定捕获块之前出现一个更通用的捕获块,它将捕获所有异常,而特定捕获块将永远不会被捕获,从而导致编译错误。
结论
Tim Corey 关于 C# 中异常处理的高级 视频指南涵盖了创建新异常、保留堆栈跟踪和有效使用多个捕获块的关键技术。 通过遵循他的最佳实践,开发人员可以创建强大的应用程序,优雅地处理异常并提供有价值的调试信息。

