为什么在C中捕获并重新引发异常?

Why catch and rethrow an exception in C#?

我在看关于可序列化DTO的文章C-数据传输对象。

这篇文章包括这段代码:

1
2
3
4
5
6
7
8
9
10
11
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;
    }
}

文章的其余部分看起来是理智和合理的(对一个noob来说),但那次尝试接球抛出了一个wtfeexception…这是否完全等同于根本不处理异常?

埃尔戈:

1
2
3
4
5
6
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++。

堆栈溢出问题重新抛出参数减去catch和不做任何事情之间的区别?似乎支持我的观点,即尝试-接球-投掷是不允许的。

编辑:

只是为将来找到这个线索的人总结一下…

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)。

参考文献:

  • msdn-异常处理
  • msdn-尝试捕获(C参考)


首先,文章中的代码做这件事的方式是邪恶的。throw ex会将异常中的调用堆栈重置到该throw语句所在的位置;丢失有关异常实际创建位置的信息。

第二,如果您只是这样捕获并重新抛出,我看不到任何附加值,那么上面的代码示例在不使用try-catch的情况下也会很好(或者,考虑到throw ex位,甚至更好)。

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

1
2
3
4
5
6
7
8
9
try
{
    // code that may throw exceptions    
}
catch(Exception ex)
{
    // add error logging here
    throw;
}


不要这样做,

1
2
3
4
5
6
7
8
try
{
...
}
catch(Exception ex)
{
   throw ex;
}

您将丢失堆栈跟踪信息…

要么这样做,

1
2
try { ... }
catch { throw; }

1
2
3
4
5
try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

如果您处理的是不同的异常,那么您可能需要重新执行的原因之一是例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
}


c(在c 6之前)不支持cil"过滤异常",vb支持,因此在c 1-5中,重新引发异常的一个原因是在catch()时没有足够的信息来确定是否要实际捕获异常。

例如,在VB中可以

1
2
3
4
5
Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 ..
End Try

…它不会处理具有不同错误代码值的MyException。在V6之前的C中,如果错误代码不是123,则必须捕获并重新抛出MyException:

1
2
3
4
5
6
7
8
9
try
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}

由于C 6.0,您可以像使用VB一样进行过滤:

1
2
3
4
5
6
7
8
try
{
  // Do stuff
}
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
  // Handle, other exceptions will be left alone and bubble up
}


我有代码的主要原因如下:

1
2
3
4
5
6
7
8
try
{
    //Some code
}
catch (Exception e)
{
    throw;
}

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

不过,在调试期间它们很好。


重新引发异常的一个有效原因可能是,您希望向异常添加信息,或者可能需要自行生成一个原始异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
  }
}


Isn't this exactly equivalent to not
handling exceptions at all?

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


你不想抛出ex-因为这会丢失调用堆栈。请参阅异常处理(msdn)。

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


人们没有提到的一点是,尽管.NET语言没有真正做出适当的区分,但在发生异常时是否应该采取行动以及是否能够解决异常的问题实际上是截然不同的问题。在许多情况下,一个人应该根据自己没有希望解决的异常来采取行动,在某些情况下,"解决"一个异常所必需的全部就是将堆栈展开到某个点——不需要采取进一步的行动。

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

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


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

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

另外,使用Throw而不是throw ex,可以保留完整的堆栈跟踪。throw ex从throw语句重新启动堆栈跟踪(我希望这是有意义的)。


捕获throw的一个可能原因是禁用堆栈中更深层次的任何异常过滤器来过滤(随机的旧链接)。但是,当然,如果这是意图的话,那里会有评论这样说。


这在库或DLL的编程函数中很有用。

此rethrow结构可用于有目的地重置调用堆栈,这样,您就可以从函数本身获得异常,而不是从函数内部的单个函数抛出异常。

我认为这只是为了让抛出的异常更干净,而不进入库的"根"中。


虽然其他许多答案都提供了很好的例子,说明为什么您可能希望捕获一个重新引发的异常,但似乎没有人提到"最终"场景。

例如,您有一个方法,其中设置了光标(例如等待光标),该方法有多个退出点(例如,if()return;),您希望确保在方法末尾重置光标。

要做到这一点,您可以在try/catch/finally中包装所有代码。在最后将光标设置回右光标。这样,您就不会隐藏任何有效的异常,而是在catch中重新执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}


在您发布的代码中的示例中,事实上,捕获异常没有意义,因为在捕获上没有任何操作,它只是重新定义,事实上,当调用堆栈丢失时,它弊大于利。

但是,如果发生异常,您会捕获一个异常来执行一些逻辑(例如关闭文件锁的SQL连接,或者只是一些日志记录),然后将其抛出到要处理的调用代码中。这在业务层中比前端代码更常见,因为您可能希望实现业务层的编码人员处理异常。

要重新迭代,在您发布的示例中捕捉异常是没有意义的。别那样做!


不好意思,许多"改进设计"的例子闻起来还是很难闻,或者极易引起误解。尝试捕获日志;抛出完全没有意义。异常日志记录应该在应用程序的中心位置进行。无论如何,异常都会在stacktrace上冒泡,为什么不将它们记录在系统边界附近的某个位置呢?

当您将上下文(即给定示例中的dto)序列化到日志消息中时,应该小心。它可以很容易地包含敏感信息,人们可能不想接触到所有可以访问日志文件的人。如果您不向异常添加任何新信息,我真的看不到异常包装的意义。好的Java有点意义,它需要调用方知道应该调用什么样的异常,然后调用代码。因为你在.NET中没有这个功能,所以包装在我见过的至少80%的情况下没有任何效果。


除了其他人所说的之外,请看我对相关问题的回答,这个问题表明捕获和重新执行不是一个"不操作"(它在vb中,但有些代码可以从vb中调用)。


大多数答案都是关于场景捕获日志的。

不要把它写在代码中,而是考虑使用AOP,特别是Postharp.diagnostic.toolkit和OnExceptionOptions includeParameterValue以及包含文档