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


557

我正在看文章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 ... 这不完全等同于根本不处理异常吗?

Ergo:

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是-no-op。


编辑:

总结一下,以便将来找到该线程的任何人...

不要

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)。


参考文献:


8
总结不错;包括finally块在内的加分。
2009年

我想补充一点,您可以使用“投掷”;通过在“抛出”之前在e.Data集合中添加发送到该方法的参数来提供更多帮助。声明
Michael Bahig 2013年

@MickTheWarMachineDesigner(兼职画家)。??您正在谈论处理Microshite Suckwell(据我所知大概从2005年开始)异常。我在谈论一般的异常处理。是的,自从我发布此《近四年》以来,我学到了一些知识……。是的,我承认您有道理,但是我认为您已经错过了真正的道理。如果你得到我的帮助?这个问题是关于C#中的GENERALIZED异常处理。更具体地说,是关于抛出异常的……。凉?
corlettk

请考虑将问题中的“编辑摘要”部分移至自己的答案。有关原因,请参阅“毫无疑问地编辑自我答案”和“ 有问题的嵌入内容”
DavidRR

2
有人没有注意到“发生排泄物”部分吗?听起来代码像个便便!
杰森·洛基·史密斯

Answers:


430

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

其次,如果您只是像这样捕获并重新抛出,我看不到有任何附加值,那么上面的代码示例如果throw ex没有try-catch 一样好(或者,如果有一点,甚至更好)。

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

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

6
@Fredrick,仅供参考(尽管您可能知道),如果您不打算使用该ex对象,则无需实例化它。
伊恩·坎贝尔

78
@Eoin:如果未实例化,将很难记录它。
山姆·艾克斯

30
是的,我认为“邪恶”是正确的……考虑一下从大量代码中抛出空指针异常的情况。该消息是原始消息,没有堆栈跟踪,您将剩下“某处为空”的信息。生产停产时不好。而且您没有几分钟或更短的时间就可以解决flamin的问题,并将其消除或纠正...良好的异常处理值得用金来解决。
corlettk

4
Java是否也是如此……“ throw”与“ throw ex”?
詹森·斯托尔兹2011年

8
@Jason,看到这个问题。在Java中,throw ex不重新启动stacktrace。
马修·弗莱申

117

不要这样

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
}

7
为什么不把catch {}扔掉呢?
AnthonyWJones

3
离开渔具{扔;}在特定异常类型捕获列表的底部,理由是它证明了作者已经考虑了该情况,尽管评论可能同样足够。当您阅读代码时不要猜测是一件好事。
annakata

87
由于某种原因,SQLException的名称使我感到困扰。
迈克尔·迈尔斯

13
这个catch(Exception){throw new Exception(...)}是永远不应该做的事情,仅仅是因为您混淆了异常信息,并使不必要的过滤变得更加困难。唯一应捕获一种异常并引发另一种异常的时间是在实现抽象层时,并且需要将提供程序特定的异常类型(例如SqlException与XmlException)转换为更通用的异常类型(例如DataLoadingException)。
jammycakes

3
@dark_perfect,您应该在方法开始时先检查该参数,然后在其中抛出ArgumentNullException(快速失败)。
Andrei Bozantan 2014年

56

C#(在C#6之前)不支持VB所支持的CIL“过滤的异常”,因此在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
}

2
Dave,但是(至少在Java中)您不会抛出“泛型” MyException,而是要定义一个SPECIFIC异常类型并将其抛出,从而允许在catch块中按类型对其进行区分...但是,是的,如果您不是异常的架构师(我在这里是在考虑JDBC的SQLException(再次是Java),那是令人反感的泛型,并公开了getErrorCode()方法...嗯...您已经明白了,就是这样我认为有一种更好的方法,如果可能的话,干杯伴侣,非常感谢您的时间Keith
corlettk 2009年

1
好吧,问题是“为什么要在C#中捕获并抛出异常?”,这就是答案。=] ...甚至使用特殊的异常,异常过滤器也很有意义:假设您正在处理SqlTimeoutException和SqlConnectionResetException的情况,它们都是SqlException。异常过滤器使您仅在两者兼有时才捕获SqlException,因此您可以“在ex为SqlTimeoutException或ex为SqlConnectionResetException时捕获SqlException,而不是用相同的处理来使try / catch混乱。” (我不是Dave btw)
bzlm

3
C#6中出现了过滤异常!
Crispalot爵士,2015年

14

我拥有类似代码的主要原因:

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

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

它们在调试期间很好。


1
是的,我要付那
笔钱

25
实际上,这不是必需的-在Visual Studio中,您可以将调试器设置为在引发异常时中断,并在检查器窗口中为您显示异常详细信息。
jammycakes

8
如果只想在调试过程中使用某些代码,请使用#if DEBUG ... #endif,而无需删除这些行
Michael Freidgeim 2012年

1
是的,我自己做了几次。时不时地会有人逃脱释放。@jammycakes Visual Studio异常中断的问题是,有时我想要的异常不是引发的唯一异常(甚至不是其类型之一)。仍然不知道带有“如果被异常跳过则中断”的断点条件。到那时,这将仍然有用。Michael Freidgeim:和#if DEBUG两者都有点混乱,坦率地说,这让我很不安...一般来说,预处理器不是我的朋友。try {} catch () {...}

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);
  }
}

谢谢,是的,异常包装(尤其是链式包装)是完全明智的……不是理智的就是捕获异常,这样您就可以舍弃堆栈痕迹,或更糟糕的是吃掉它。
corlettk

10

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

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


8

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

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


使用throw ex时,您不会丢失整个调用堆栈,而只是从异常在其调用堆栈上方发生的那一刻起,丢失了一部分调用堆栈。但是,您保留了将异常抛出到客户端调用它的方法的调用堆栈。实际上,在某些情况下,您可能会使用它,否则Microsoft的好人不会允许它。也就是说,我没有使用过。要记住的另一个问题是抛出异常的代价很高。仅出于非常合理的理由执行此操作。记录我想会是合理的,等等
查尔斯·欧文

5

人们没有提到的一点是,虽然.NET语言并没有真正区分,但是在发生异常时是否应该采取措施以及是否可以解决这一问题实际上是不同的问题。在很多情况下,一个人应该基于异常采取行动,一个人没有希望解决的希望,并且在某些情况下,“解决”异常所需要的所有事情就是将堆栈展开到某个特定点,而无需采取进一步的措施。

由于人们通常只应该“抓住”一个人可以“处理”的事情,因此,许多代码在异常发生时应该采取的措施就没有了。例如,许多代码将获取一个锁,将受保护对象“临时”置于违反其不变式的状态,然后将其置于合法状态,然后在其他人看不到该对象之前释放该锁。如果在对象处于危险无效状态时发生异常,通常的做法是在对象仍处于该状态的情况下释放锁。更好的模式是在对象处于“危险”状态时发生异常,从而使锁无效,因此将来任何尝试获取该锁的操作都会立即失败。

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


5

当您对库或dll进行编程时,这可能会很有用。

此重新抛出结构可用于有目的地重置调用堆栈,以便您不必从函数内部的单个函数中看到异常,而可以看到该异常。

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


3

引发异常的一个可能原因是禁用了堆栈深处的所有异常过滤器,使其无法过滤掉(随机旧链接)。但是,当然,如果那是意图,那么那里会有一条评论说。


直到我阅读了链接,我才知道您在干什么...我仍然不确定您在干什么...我完全不熟悉VB.NET。我认为这会导致总和被报告为“不一致”,对吧?...我是静态方法的忠实拥护者。除了它们简单之外,如果您将属性的设置与代码分开,则不一致的可能性就较小实际的工作。堆栈是“自我清洁”。
corlettk

3
人们期望当他们写“ try {Foo();}最后{Bar();}”时,Foo和Bar之间没有任何关系。但是这是错误的; 如果调用方添加了一个异常过滤器,并且中间没有'catch'且Foo()抛出,则来自调用方的其他一些随机代码将在您的finally(Bar)运行之前运行。如果您破坏了不变式或提高了安全性,这很不好,因为您希望它们能够在“ finally”最后“立即”恢复到正常状态,并且其他代码都不会看到临时更改。
布赖恩

3

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

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

也可以这样做,Throw而不是Throw Ex让您保留整个堆栈的踪迹。Throw ex从throw语句重新启动堆栈跟踪(我希望这是有道理的)。


3

尽管许多其他答案都提供了很好的示例,说明了为什么您可能想捕获重新引发的异常,但似乎没有人提到“最终”情况。

这方面的一个示例是,您有一个方法来设置光标(例如设置为等待光标),该方法有多个退出点(例如if()return;),并且您想确保在方法的结尾。

为此,您可以将所有代码包装在try / catch / finally中。在最后设置光标回到右光标。为了避免隐藏任何有效的异常,请将其重新放入捕获中。

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

1
在历史上catch是必不可少的一部分try...finally,还是在此示例中发挥了作用?-我只是仔细检查了一下,即使try {} finally {}没有catch块也可以使用。
塞比

2

实际上,在您发布的代码示例中,捕获异常没有任何意义,因为在捕获上没有执行任何操作,只是重新抛出了异常,实际上,这样做的弊大于利,因为丢失了调用堆栈。

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

再次重申,在您发布的示例中,捕获异常没有任何意义。不要那样做!


1

抱歉,但是许多示例,例如“改进的设计”仍然令人发指,甚至可能极具误导性。尝试{}捕获{日志;throw}毫无意义。异常日志记录应在应用程序内部的中央位置进行。无论如何,异常都会使stacktrace冒泡,为什么不将它们记录到某个位置并靠近系统边界呢?

当将上下文(即给定示例中的DTO)仅序列化到日志消息中时,应谨慎使用。它可以轻松地包含敏感信息,而这些信息可能不希望触及所有可以访问日志文件的人。而且,如果您不向异常添加任何新信息,我真的看不到异常包装的意义。好的旧Java有一定的意义,它要求调用者知道在调用代码之前应该期望什么样的异常。由于您在.NET中没有此功能,因此在至少80%的情况下,包装都无济于事。


谢谢您的想法,乔。在Java(我想是C#)中,我很想看到一个类级别的注释@FaultBoundary,它强制所有异常(包括未检查的异常类型)被捕获或声明为抛出。我将在每个体系结构层的公共接口上使用此注释。因此,@ FaultBoundary ThingDAO接口将无法泄漏实现细节,例如SQLException,NPE或AIOB。相反,将记录“因果”堆栈跟踪并抛出DAOSystemException ...我将系统异常定义为“永久致命”。
corlettk

5
有很多原因可以捕获,记录然后重新抛出。具体来说,如果包含捕获日志的方法包含信息,那么一旦您退出该方法,就会丢失。稍后可能会处理该错误,但不会记录该错误,并且您已经丢失了有关系统缺陷的信息。
安迪

1
这是Exception类的Data属性很方便的地方-捕获所有本地信息以进行常规日志记录。这篇文章最初把它带到我的注意: blog.abodit.com/2010/03/...
McGuireV10

1

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


尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能会失效。- 评分
LHIOUI

@HamzaLH,我同意这不是写得很好的答案,但是它具有与其他答案和赞成票不同的信息。所以我不明白,为什么您建议删除它?“关于主题的简短答案并给出解决方案仍然是答案。” 来自 meta.stackexchange.com/questions/226258/…–
迈克尔·

这是仅链接的答案
LHIOUI

1.仅链接的答案应更改为注释,而不是删除。2.这是对其他SO问题的引用,而不是对外部站点的引用,外部站点被认为随着时间的推移不太可能被破坏。3.它有一些额外的说明,这使得它无法“链接只有” -见meta.stackexchange.com/questions/225370/...
迈克尔Freidgeim

1

关于场景捕获日志重新抛出的大多数答案。

与其在您的代码中编写它,不如考虑使用AOP,尤其是使用具有OnExceptionOptions IncludeParameterValue和IncludeThisArgument的Postsharp.Diagnostic.Toolkit


尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能会失效。- 点评
东尼·董

@TonyDong,我同意这不是一个写得很好的答案,但它具有与其他答案和赞成票不同的信息。所以我不明白,为什么您建议删除它?顺便说一句,5年后的链接仍然有效。“关于主题的简短答案并给出解决方案仍然是答案。” 来自 meta.stackexchange.com/questions/226258/…–
迈克尔·

Stackoverflow仅具有此建议。
东尼·董

@TonyDong,如果答案不是绝对没有用,那么您应该选择“看起来不错”
Michael Freidgeim 18-10-28

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.