为什么在c#中捕获并重新抛出异常?

我正在查看关于可序列化dto的文章 c# -数据传输对象

这篇文章包含了下面这段代码:

public static string SerializeDTO(DTO dto) {
try {
XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
StringWriter sWriter = new StringWriter();
xmlSer.Serialize(sWriter, dto);
return sWriter.ToString();
}
catch(Exception ex) {
throw ex;
}
}

文章的其余部分看起来理智而合理(对于新手来说),但是try-catch-throw抛出了一个WtfException…# EYZ0

因此:

public static string SerializeDTO(DTO dto) {
XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
StringWriter sWriter = new StringWriter();
xmlSer.Serialize(sWriter, dto);
return sWriter.ToString();
}

还是我错过了c#中错误处理的一些基本内容?它与Java非常相似(减去受控异常),不是吗?... 也就是说,它们都改进了c++。

Stack Overflow问题重新抛出无参数catch和什么都不做之间的区别?< / >似乎支持了我的论点,即try-catch-throw是一个没有操作的问题。


编辑:

只是为了给将来找到这条线索的人做个总结…

try {
// Do stuff that might throw an exception
}
catch (Exception e) {
throw e; // This destroys the strack trace information!
}

堆栈跟踪信息对于确定问题的根本原因至关重要!

try {
// Do stuff that might throw an exception
}
catch (SqlException e) {
// Log it
if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound.
// Do special cleanup, like maybe closing the "dirty" database connection.
throw; // This preserves the stack trace
}
}
catch (IOException e) {
// Log it
throw;
}
catch (Exception e) {
// Log it
throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java).
}
finally {
// Normal clean goes here (like closing open files).
}

在不太特定的异常之前捕获更特定的异常(就像Java一样)。


引用:

281020 次浏览

你不会想要抛出ex -,因为这会丢失调用堆栈。参见异常处理 (MSDN)。

是的,尝试…Catch没有做任何有用的事情(除了丢失调用堆栈之外——所以它实际上更糟——除非出于某种原因您不想公开此信息)。

第一个;本文中代码的实现方式是邪恶的。throw ex将重置异常中的调用堆栈到该throw语句所在的位置;丢失有关异常实际创建位置的信息。

其次,如果您只是像这样捕获并重新抛出,我认为没有任何附加价值,上面的代码示例没有try-catch也一样好(或者,给定throw ex位,甚至更好)。

但是,在某些情况下,您可能希望捕获并重新抛出异常。日志可能是其中之一:

try
{
// code that may throw exceptions
}
catch(Exception ex)
{
// add error logging here
throw;
}

重新抛出异常的一个有效原因可能是您想要向异常添加信息,或者可能将原始异常包装在您自己制作的异常中:

public static string SerializeDTO(DTO dto) {
try {
XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
StringWriter sWriter = new StringWriter();
xmlSer.Serialize(sWriter, dto);
return sWriter.ToString();
}
catch(Exception ex) {
string message =
String.Format("Something went wrong serializing DTO {0}", DTO);
throw new MyLibraryException(message, ex);
}
}

不要这样做,

try
{
...
}
catch(Exception ex)
{
throw ex;
}

您将丢失堆栈跟踪信息…

无论做什么,

try { ... }
catch { throw; }

try { ... }
catch (Exception ex)
{
throw new Exception("My Custom Error Message", ex);
}
您可能想要重新抛出的原因之一是,如果您正在处理不同的异常,对于 例如< / p >
try
{
...
}
catch(SQLException sex)
{
//Do Custom Logging
//Don't throw exception - swallow it here
}
catch(OtherException oex)
{
//Do something else
throw new WrappedException("Other Exception occured");
}
catch
{
System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack");
throw; //Chuck everything else back up the stack
}

这取决于您在catch块中所做的事情,以及您是否希望将错误传递给调用代码。

您可能会说Catch io.FileNotFoundExeption ex,然后使用替代文件路径或其他类似的路径,但仍然会抛出错误。

此外,使用Throw而不是Throw Ex允许您保留完整的堆栈跟踪。Throw ex从Throw语句重新启动堆栈跟踪(我希望这有意义)。

这不是完全等同于not吗 处理异常?< / p >

不完全一样,这是不一样的。它重置异常的堆栈跟踪。 虽然我同意这可能是一个错误,因此是一个坏代码的例子

在你发布的代码中的例子中,事实上,捕捉异常没有任何意义,因为在捕捉上没有做任何事情,它只是重新抛出,事实上它弊大于利,因为调用堆栈丢失了。

但是,您可以捕获异常来执行一些逻辑(例如关闭文件锁的sql连接,或者只是一些日志记录),在发生异常时将其扔回调用代码来处理。这在业务层中比在前端代码中更常见,因为您可能希望实现业务层的编码器处理异常。

在你发布的例子中,捕捉异常是没有意义的。不要那样做!

c#(在c# 6之前)不支持CIL“过滤异常”,而VB支持,因此在c# 1-5中,重新抛出异常的一个原因是在catch()时没有足够的信息来确定是否要真正捕获异常。

例如,在VB中你可以做到

Try
..
Catch Ex As MyException When Ex.ErrorCode = 123
..
End Try

...它不会处理具有不同ErrorCode值的myexception。在v6之前的c#中,如果ErrorCode不是123,你必须捕获并重新抛出MyException:

try
{
...
}
catch(MyException ex)
{
if (ex.ErrorCode != 123) throw;
...
}

从c# 6.0开始,你可以过滤和VB一样:

try
{
// Do stuff
}
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
// Handle, other exceptions will be left alone and bubble up
}

使用catch-throw的一个可能原因是禁用堆栈更深层次的任何异常过滤器(随机旧链接)。当然,如果这是他们的意图,肯定会有评论这么说。

抱歉,但许多“改进设计”的例子仍然很糟糕,或者可能会极具误导性。有try {} catch {log;Throw}完全没有意义。异常日志应该在应用程序的中心位置完成。无论如何,异常都会出现在堆栈跟踪中,为什么不将它们记录在系统边界附近的某个地方呢?

当您将上下文(例如,在一个给定的例子中是DTO)序列化到日志消息中时,应该注意。它可以很容易地包含敏感信息,您可能不想让所有可以访问日志文件的人获得这些信息。如果不向异常添加任何新信息,我真的不明白异常包装的意义。优秀的老Java在这方面有一定的意义,它要求调用者知道在调用代码时应该期望出现什么样的异常。因为你在。net中没有这个功能,所以在我所见过的至少80%的情况下,包装没有任何好处。

我使用如下代码的主要原因:

try
{
//Some code
}
catch (Exception e)
{
throw;
}

是我可以在catch中有一个断点,它有一个实例化的异常对象。在开发/调试时,我经常这样做。当然,编译器会对所有未使用的e给出警告,理想情况下,应该在发布版本之前删除它们。

不过,在调试过程中它们很好用。

除了其他人所说的,请参阅我的答案的相关问题,其中显示捕获和重新抛出不是一个无操作(它在VB中,但一些代码可以从VB调用c#)。

大多数答案都是关于catch-log-rethrow的。

而不是写在你的代码中考虑使用AOP,特别是Postsharp.Diagnostic.Toolkit与OnExceptionOptions IncludeParameterValue和 IncludeThisArgument < / p >

人们没有提到的一点是,虽然。net语言并没有真正做出适当的区分,但当异常发生时是否应该采取行动,以及是否应该解决,实际上是两个不同的问题。在许多情况下,应该根据无法解决的异常采取行动,在某些情况下,“解决”异常所必需的只是将堆栈展开到某个点——不需要进一步的行动。

由于人们普遍认为应该只“捕获”自己能够“处理”的东西,因此许多应该在异常发生时采取行动的代码并没有这样做。例如,许多代码将获得一个锁,将受保护的对象“暂时”置于违反其不变量的状态,然后将其置于合法状态,然后在其他人可以看到该对象之前释放锁。如果在对象处于危险无效状态时发生异常,通常的做法是在对象仍处于该状态时释放锁。更好的模式是,在对象处于“危险”状态时出现异常,明确地使锁失效,这样以后任何获取它的尝试都将立即失败。这种模式的一致使用将极大地提高所谓的“Pokemon”异常处理的安全性,在我看来,这种异常处理的坏名声主要是因为代码允许异常渗透而不首先采取适当的操作。

在大多数。net语言中,代码基于异常采取操作的唯一方法是catch它(即使它知道它不会解决异常),执行有问题的操作,然后重新throw)。如果代码不关心抛出什么异常,另一种可能的方法是使用ok标志和try/finally块;将ok标记设置为块前的false,块退出前的true,以及块内的任何return之前。然后,在finally中,假设如果ok未设置,则一定发生了异常。这种方法在语义上比catch/throw更好,但它很丑,而且比它应有的可维护性更差。

虽然许多其他答案提供了很好的示例,说明为什么您可能希望捕获重抛出异常,但似乎没有人提到“最终”场景。

举个例子,你有一个方法,你在其中设置了游标(例如一个等待游标),该方法有几个出口点(例如if () return;),你想确保游标在方法结束时被重置。

为此,您可以将所有代码包装在try/catch/finally中。在最后将光标设置回右光标。这样就不会隐藏任何有效的异常,在catch中重新抛出它。

try
{
Cursor.Current = Cursors.WaitCursor;
// Test something
if (testResult) return;
// Do something else
}
catch
{
throw;
}
finally
{
Cursor.Current = Cursors.Default;
}

当您为库或dll编程函数时,这可能很有用。

此重抛出结构可用于有目的地重置调用堆栈,以便从函数本身获取异常,而不是从函数内部的单个函数中查看抛出的异常。

我认为这样做只是为了让抛出的异常更清晰,并且不会进入库的“根”。

当您没有特定的代码来处理当前的异常时,或者当您有一个逻辑来处理特定的错误情况但想要跳过所有其他情况时,通过throw重新抛出异常非常有用。

例子:

string numberText = "";
try
{
Console.Write("Enter an integer: ");
numberText = Console.ReadLine();
var result = int.Parse(numberText);


Console.WriteLine("You entered {0}", result);
}
catch (FormatException)
{
if (numberText.ToLowerInvariant() == "nothing")
{
Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}
else
{
throw;
}
}
finally
{
Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

然而,也有另一种方式的做法,在catch块中使用有条件的条款:

string numberText = "";
try
{
Console.Write("Enter an integer: ");
numberText = Console.ReadLine();
var result = int.Parse(numberText);


Console.WriteLine("You entered {0}", result);
}
catch (FormatException) when (numberText.ToLowerInvariant() == "nothing")
{
Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}
finally
{
Console.WriteLine("Freed some resources.");
}
Console.ReadKey();
此机制比重新抛出异常更有效,因为 的。net运行时不需要重新构建异常对象