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

cor*_*ttk 528 c# exception-handling try-catch

我正在查看文章C# - 可序列化DTO上的数据传输对象.

这篇文章包括这段代码:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

本文的其余部分看起来很合理(对于菜鸟),但是try-catch-throw会抛出一个WtfException ... 这不完全等同于根本不处理异常吗?

人机工程学:

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

或者我错过了C#中错误处理的基本内容?它与Java几乎相同(减去已检查的异常),不是吗?......也就是说,他们都改进了C++.

Stack Overflow问题重新抛出无参数捕获和不执行任何操作之间的区别?似乎支持我的观点,即try-catch-throw是一个无操作.


编辑:

只是为了总结未来发现这个主题的人...

不要

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

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

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).
}
Run Code Online (Sandbox Code Playgroud)

在不太具体的异常之前捕获更具体的异常(就像Java一样).


参考文献:

Fre*_*örk 414

第一; 文章中代码的表现方式是邪恶的.throw ex将异常中的调用堆栈重置为此throw语句的位置; 丢失有关实际创建异常的位置的信息.

第二,如果你只是抓住并重新抛出这样的东西,我认为没有附加价值,上面的代码示例在throw ex没有try-catch的情况下也会同样好(或者,有点,甚至更好).

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

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}
Run Code Online (Sandbox Code Playgroud)

  • @Eoin:如果没有实例化它就很难记录它. (74认同)
  • 是的,我认为"邪恶"是正确的...考虑从大量代码抛出空指针异常的情况.消息是vanilla,没有堆栈跟踪,你留下了"某处某处为null".生产停止时效果不佳; 并且你没有时间或更少的时间来解决这个问题,并解雇或纠正它......良好的异常处理值得它的重量是黄金. (29认同)
  • @Jason,见[这个问题](http://stackoverflow.com/questions/1097527/rethrowing-exceptions-in-java).在Java中,`throw ex`不会重新启动堆栈跟踪. (8认同)
  • @Fredrick,只是fyi(虽然你可能知道)如果你不打算使用那个'ex`对象,那么就没有必要实例化它了. (5认同)
  • Java也是如此......"抛出"对"抛出前"? (4认同)
  • @Eoin:我知道这一点.在这种情况下,我建议捕获异常,以便在重新抛出之前对其执行某些操作(例如日志记录),然后我将需要异常实例.虽然好点. (3认同)
  • @divo; 同意,它有点强大,同意.但是我已经多次错误地看到了这一点(而不是一次,我认为,这是一个好主意),所以我想做一个明确的陈述. (3认同)
  • 在您的日志记录代码抛出异常的情况下会发生什么? (2认同)

Eoi*_*ell 112

不要这样做,

try 
{
...
}
catch(Exception ex)
{
   throw ex;
}
Run Code Online (Sandbox Code Playgroud)

您将丢失堆栈跟踪信息......

要么,

try { ... }
catch { throw; }
Run Code Online (Sandbox Code Playgroud)

要么

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}
Run Code Online (Sandbox Code Playgroud)

您可能想要重新抛出的原因之一是,如果您正在处理不同的例外,例如

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
}
Run Code Online (Sandbox Code Playgroud)

  • 出于某种原因,SQLException的名称困扰着我. (84认同)
  • 那个catch(Exception){throw new Exception(...)}是你应该**永远不会做的事情,只是因为你对异常信息进行了混淆,并且在调用堆栈中进一步使异常过滤变得不必要.您应该捕获一种类型的异常并抛出另一种异常的唯一一次是在实现抽象层时,您需要将特定于提供者的异常类型(例如,SqlException与XmlException)转换为更通用的异常(例如DataLoadingException). (11认同)
  • 为什么不完全离开catch {throw}? (6认同)
  • 离开catch {throw; 在一个特定的异常类型catch列表的底部,它证明了作者认为是这种情况,尽管评论可能同样足够.不猜你何时阅读代码是一件好事. (3认同)
  • @dark_perfect你应该在方法的开头预先检查那个参数,并在那里抛出ArgumentNullException(快速失败). (3认同)
  • 我有一种感觉,这段代码的'WrappedException`会忘记包装.作者是否打算将原始异常作为InnerException放在该构造函数中? (2认同)
  • 在[MS约定](https://msdn.microsoft.com/en-us/library/ms229043.aspx)下,只有两个字母的首字母缩略词应该保持大写(`IOException`),更长的首字母缩略词应该是PascalCased([`SqlException] `](https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlexception.aspx)).与Java不同(参见[`SQLException`](http://docs.oracle.com/javase/7/docs/api/java/sql/SQLException.html)).这就是为什么`SQLException`困扰你的原因,@迈克尔迈尔斯. (2认同)

bzl*_*zlm 55

C#(在C#6之前)不支持CIL"过滤异常",这是VB所做的,所以在C#1-5中重新抛出异常的一个原因是你在catch()时没有足够的信息确定是否要实际捕获异常.

例如,在VB中你可以做到

Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 .. 
End Try
Run Code Online (Sandbox Code Playgroud)

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

try 
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}
Run Code Online (Sandbox Code Playgroud)

从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
}
Run Code Online (Sandbox Code Playgroud)

  • 过滤的异常即将发布在C#6中! (3认同)
  • Dave,但是(至少在java中)你不会抛出"泛型"MyException,你要定义一个SPECIFIC异常类型并抛出它,允许它在catch块中按类型区分......但是是的,如果你不是异常的架构师(我在这里想到JDBC的SQLException(Java),这是令人厌恶的通用,并暴露了getErrorCode()方法......嗯......你有一个观点,它只是那个我认为在可能的情况下有更好的方法.干杯配偶.我非常感谢你的时间.基思. (2认同)

小智 14

我的主要原因是:

try
{
    //Some code
}
catch (Exception e)
{
    throw;
}
Run Code Online (Sandbox Code Playgroud)

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

他们在调试时很好.

  • 实际上,这不是必需的 - 在Visual Studio中,您可以将调试器设置为在抛出异常时中断,并在检查器窗口中为您显示异常详细信息. (25认同)
  • 如果你想在调试期间只使用一些代码,使用#if DEBUG ... #endif,你不需要删除这些行 (8认同)

edo*_*oft 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) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}
Run Code Online (Sandbox Code Playgroud)


Arj*_*nbu 10

这不完全等同于根本不处理异常吗?

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


Dun*_*can 8

你不想抛出前 - 因为这将失去调用堆栈.请参阅异常处理(MSDN).

是的,try ... catch没有任何用处(除了丢失调用堆栈 - 所以它实际上更糟 - 除非出于某种原因你不想公开这些信息).


小智 6

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

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

我认为这只是用来使抛出的异常更干净,并且不会进入库的“根”。


sup*_*cat 5

人们没有提到的一点是,虽然.NET语言并没有真正做出恰当的区分,但是一个人是否应该在发生异常时采取行动以及是否会解决它的问题实际上是截然不同的问题.在许多情况下,人们应该根据无法解决的异常采取行动,并且在某些情况下,"解决"异常所需的一切都是将堆栈展开到某一点 - 不需要进一步的操作.

由于人们应该只"抓住"可以"处理"的事物的共同智慧,许多应该在异常发生时采取行动的代码不会.例如,许多代码将获取锁,将受保护的对象"暂时"置于违反其不变量的状态,然后将其置于合法状态,然后在其他任何人都可以看到该对象之前释放锁定.如果在对象处于危险无效状态时发生异常,通常的做法是在对象仍处于该状态时释放锁定.一个更好的模式是在对象处于"危险"状态时发生异常,明确地使锁无效,因此将来任何获取它的尝试都将立即失败.

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