641

我正在看文章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;
    }
}

文章的其余部分看起来很理智和合理(对于菜鸟来说),但是 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();
}

还是我错过了一些关于 C# 中错误处理的基本知识?它与 Java 几乎相同(减去检查的异常),不是吗?...也就是说,他们都改进了 C++。

堆栈溢出问题重新抛出无参数捕获和不做任何事情之间的区别?似乎支持我的论点,即 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 一样)。


参考:

4

17 回答 17

486

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

其次,如果你只是像这样捕获并重新抛出,我认为没有任何附加值,上面的代码示例在throw ex没有 try-catch 的情况下也一样好(或者,考虑到一点,甚至更好)。

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

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}
于 2009-05-19T08:02:45.000 回答
127

不要这样做,

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

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

要么做,

try { ... }
catch { throw; }

或者

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

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

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
}
于 2009-05-19T08:03:23.267 回答
60

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

例如,在 VB 中你可以做

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

...它不会处理具有不同 ErrorCode 值的 MyExceptions。在 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
}
于 2009-05-19T08:20:21.640 回答
18

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

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

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

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

于 2009-09-18T09:50:33.167 回答
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);
  }
}
于 2009-05-19T08:03:22.300 回答
10

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

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

于 2009-05-19T08:11:11.770 回答
8

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

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

于 2009-05-19T08:02:32.690 回答
6

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

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

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

于 2018-05-27T15:35:30.960 回答
5

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

由于人们应该只“捕获”可以“处理”的东西这一普遍智慧,许多在发生异常时应该采取行动的代码却没有。例如,很多代码会获取锁,将受保护的对象“临时”置于违反其不变量的状态,然后将其置于合法状态,然后在其他人看到该对象之前释放锁。如果在对象处于危险无效状态时发生异常,通常的做法是在对象仍处于该状态时释放锁。更好的模式是在对象处于“危险”条件时发生异常,明确使锁无效,因此任何未来获取它的尝试都将立即失败。

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

于 2013-10-14T16:56:17.780 回答
4

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

例如,您有一个设置光标的方法(例如设置为等待光标),该方法有几个退出点(例如 if () return;),并且您希望确保光标在方法结束。

为此,您可以将所有代码包装在 try/catch/finally 中。在finally中将光标设置回右光标。为了不掩埋任何有效的异常,请在 catch 中重新抛出它。

try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}
于 2016-08-09T11:47:08.870 回答
3

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

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

还做Throw而不是Throw Ex允许您保留完整的堆栈跟踪。Throw ex 从 throw 语句重新启动堆栈跟踪(我希望这是有道理的)。

于 2009-05-19T08:04:03.110 回答
3

catch-throw 的一个可能原因是禁用堆栈更深处的任何异常过滤器以防止向下过滤(随机旧链接)。但是,当然,如果这是意图,那里会有评论说这样的话。

于 2009-07-30T15:12:20.113 回答
2

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

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

尽管在您发布的示例中捕获异常没有意义,但要重新迭代。不要那样做!

于 2009-05-19T08:19:11.677 回答
1

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

当您将上下文(即一个给定示例中的 DTO)序列化到日志消息中时,应谨慎使用。它可以很容易地包含敏感信息,可能不想到达所有可以访问日志文件的人的手中。而且,如果您不向异常添加任何新信息,我真的看不出异常包装的意义。好的旧 Java 对此有一定的意义,它要求调用者知道在调用代码时应该期待什么样的异常。由于您在 .NET 中没有此功能,因此在我见过的至少 80% 的情况下,包装没有任何好处。

于 2009-09-16T07:22:00.817 回答
1

除了其他人所说的之外,请参阅对相关问题的回答,该问题表明捕获和重新抛出不是无操作(它在 VB 中,但某些代码可以从 VB 调用 C#)。

于 2009-09-22T19:53:01.980 回答
1

大多数答案都在谈论场景捕获日志重新抛出。

不要在代码中编写它,而是考虑使用 AOP,特别是带有 OnExceptionOptions IncludeParameterValue 和 IncludeThisArgument的Postsharp.Diagnostic.Toolkit

于 2012-12-25T22:41:10.177 回答
1

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 运行时不必在重新抛出异常对象之前重新构建它。

于 2019-12-09T17:08:38.623 回答