【问题标题】:Why catch and rethrow an exception in C#?为什么要在 C# 中捕获并重新抛出异常?
【发布时间】:2010-10-27 06:12:00
【问题描述】:

我正在查看关于可序列化 DTO 的文章 C# - Data Transfer Object

文章中包含了这段代码:

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

堆栈溢出问题The difference between re-throwing parameter-less catch and not doing anything? 似乎支持我的论点,即 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)。


参考资料:

【问题讨论】:

  • 好总结;包含 finally 块的额外积分。
  • 我想补充一点,你可以使用“throw;”通过在“抛出”之前添加发送到 e.Data 集合中的方法的参数来更有帮助;声明
  • @MickTheWarMachineDesigner(和兼职画家)。嗯?您正在谈论处理 Microshite Suckwell(据我所知,可能是 2005 年以后)异常。我说的是一般的异常处理。是的,自从我将近四年前发布这篇文章以来,我已经学到了一些……但是,是的,我承认你的观点是正确的,但我认为你错过了真正的观点;如果你明白我的意思?这个问题是关于 C# 中的 GENERALIZED 异常处理;更具体地说,关于重新抛出各种异常......酷吗?
  • 请考虑将问题中的编辑摘要部分移至其自己的答案。原因见Editing self-answer out of questionAnswer embedded in question
  • 没有人注意到“排泄物发生”部分吗?听起来代码像是被大便了!

标签: c# exception-handling try-catch


【解决方案1】:

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

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

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

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

【讨论】:

  • @Fredrick,仅供参考(尽管您可能知道)如果您不打算使用该 ex 对象,则无需实例化它。
  • @Eoin:如果它没有被实例化,那么记录它会相当困难。
  • 是的,我认为“邪恶”是正确的......考虑从大量代码中某处抛出空指针异常的情况。该消息是香草的,没有堆栈跟踪,您会留下“某处为空”。生产结束时不好;并且您没有几分钟或更少的时间来解决 flamin 问题,并消除或纠正它......良好的异常处理值得它的黄金重量。
  • Java 也是如此……“throw”与“throw ex”?
  • @Jason,见this question。在 Java 中,throw ex 不会重新启动堆栈跟踪。
【解决方案2】:

别这样,

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
}

【讨论】:

  • 为什么不完全放弃 catch { throw }?
  • 由于某种原因,SQLException 的名称让我很困扰。
  • 那个 catch (Exception) { throw new Exception(...) } 是你应该永远,永远,永远做的事情,仅仅因为你混淆了异常信息并使异常过滤在调用堆栈中进一步增加不必要的困难。您应该捕获一种类型的异常并抛出另一种异常的唯一时间是当您实现抽象层并且需要将特定于提供程序的异常类型(例如 SqlException 与 XmlException)转换为更通用的异常类型(例如 DataLoadingException)时。跨度>
  • 我感觉这段代码的WrappedException 忘记了换行。作者是否打算将原始异常作为 InnerException 放入该构造函数中?
  • MS convention 下,只有两个字母的首字母缩写词应保持大写 (IOException),较长的首字母缩写词应为 PascalCased (SqlException)。与 Java 不同(参见 SQLException)。这可能就是SQLException 困扰你的原因,@MichaelMyers。
【解决方案3】:

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

Since C# 6.0 you can filter 和 VB 一样:

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

【讨论】:

  • Dave,但是(至少在 java 中)你不会抛出一个“通用”MyException,你会定义一个特定的异常类型并抛出它,允许它在catch 块...但是,是的,如果您不是异常的架构师(我在这里想的是 JDBC 的 SQLException(再次是 Java),这是令人作呕的通用方法,并且公开了 getErrorCode() 方法...嗯...您有道理,只是我认为在可能的情况下有更好的方法来做到这一点。Cheers Mate。非常感谢你的时间。Keith。
  • 嗯,问题是“为什么在 C# 中捕获并重新抛出异常?”,这是一个答案。 =] ...即使有专门的异常,异常过滤器也是有意义的:考虑一下你在处理SqlTimeoutException和SqlConnectionResetException的情况,它们都是SqlException。异常过滤器允许您仅在它是这两个之一时捕获 SqlException,因此您可以“在 ex 是 SqlTimeoutException 或 ex 是 SqlConnectionResetException 时捕获 SqlException ex”,而不是使用对这两个相同的处理来弄乱您的 try/catch。 (顺便说一句,我不是戴夫)
  • 过滤的异常将在 C# 6 中出现!
【解决方案4】:

我有这样的代码的主要原因:

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

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

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

【讨论】:

  • 是的,我会支付那个,但是是的,你不会希望在 published 代码中看到它... ergo:我会羞于发布它;-)
  • 实际上,这不是必需的——在 Visual Studio 中,您可以将调试器设置为在抛出异常时中断,并在检查器窗口中为您显示异常详细信息。
  • 如果您只想在调试期间使用某些代码,请使用 #if DEBUG ... #endif ,您不需要删除这些行
  • 是的,我自己也做过几次。时不时有人会逃到释放。 @jammycakes Visual Studio 异常中断的问题是,有时我想要的异常不是唯一一个(甚至只是它的一种类型)被抛出。仍然不知道“如果被异常跳过则中断”的断点条件。到那时,这将仍然有用。 Michael Freidgeim:#if DEBUG 周围的 try {} catch () {...} 有点混乱,坦率地说,让我感到恶心......一般来说,预处理器不是我的朋友。
【解决方案5】:

重新抛出异常的正当理由可能是您想向异常添加信息,或者可能将原始异常包装在您自己的制作中:

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

【讨论】:

  • 谢谢,是的,异常包装(尤其是链式)是完全理智的......不理智的是捕获异常只是为了让你可以丢弃堆栈跟踪,或者更糟的是,吃掉它。
【解决方案6】:

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

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

【讨论】:

    【解决方案7】:

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

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

    【讨论】:

    • 当您使用 throw ex 时,您不会丢失整个调用堆栈,您只会丢失调用堆栈中从异常发生点开始的调用堆栈更高的部分。但是您将调用堆栈从引发异常的方法保留到客户端调用它的位置。实际上可能存在您会使用它的用例,否则微软的好人不会允许它。就是说,我没用过。另一个要记住的问题是抛出异常是昂贵的。仅出于非常正当的理由才这样做。我认为记录是合理的,等等。
    【解决方案8】:

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

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

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

    【讨论】:

      【解决方案9】:

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

      由于人们应该只“捕获”可以“处理”的事物这一普遍观点,因此许多在发生异常时应该采取行动的代码却没有。例如,很多代码会获取锁,将受保护的对象“临时”置于违反其不变量的状态,然后将其置于合法状态,然后在其他人看到该对象之前释放锁。如果在对象处于危险无效状态时发生异常,通常的做法是在对象仍处于该状态时释放锁。更好的模式是在对象处于“危险”条件时发生异常,明确使锁无效,因此任何未来获取它的尝试都将立即失败。一致使用这种模式将极大地提高所谓的“Pokemon”异常处理的安全性,恕我直言,这种处理的名声不好,主要是因为代码允许异常在没有先采取适当措施的情况下渗透。

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

      【讨论】:

        【解决方案10】:

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

        这方面的一个例子是,您有一个设置光标的方法(例如设置为等待光标),该方法有几个退出点(例如 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;
        }
        

        【讨论】:

        • catch 在历史上是try...finally 的强制部分,还是在此示例中发挥功能作用? - 我只是仔细检查了一下,我可以在没有 catch 块的情况下使用try {} finally {}
        【解决方案11】:

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

        【讨论】:

        • 直到我阅读了链接,我才知道你在说什么……而且我仍然不确定你在说什么……我完全不熟悉 VB。网。我认为这会导致总和被报告为“不一致”,对吧?...我是静态方法的忠实粉丝..除了它们很简单之外,如果您将属性设置与代码分开,则出现不一致的可能性会更小哪个做实际工作。堆栈是“自我清洁”的。
        • 人们期望当他们编写“try { Foo(); } finally { Bar(); }”时,Foo 和 Bar 之间没有任何运行。但是这是错误的;如果您的调用者添加了一个异常过滤器,并且没有干预“catch”,并且 Foo() 抛出,那么来自调用者的其他一些随机代码将在您的 finally (Bar) 运行之前运行。如果您破坏了不变量或提高了安全性,这是非常糟糕的,期望它们会在 finally 中“立即”恢复正常,并且没有其他代码会看到临时更改。
        【解决方案12】:

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

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

        同时使用Throw 而不是Throw Ex 可以让您保留完整的堆栈跟踪。 Throw ex 从 throw 语句中重新启动堆栈跟踪(我希望这是有道理的)。

        【讨论】:

          【解决方案13】:

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

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

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

          【讨论】:

            【解决方案14】:

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

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

            【讨论】:

            • 谢谢你的想法乔。在 Java(和 C#,我想)中,我希望看到一个类级别的注释 @FaultBoundary,它强制所有异常(包括未经检查的异常类型)被捕获或声明以被抛出。我会在每个架构层的公共接口上使用这个注释。因此,@FaultBoundary ThingDAO 接口将无法泄露 SQLException、NPE 或 AIOB 等实现细节。相反,将记录“因果”堆栈跟踪并抛出 DAOSystemException ......我将系统异常定义为“永久致命”。
            • 有很多理由去捕捉、记录,然后重新抛出。特别是如果带有 catch 日志的方法有信息,一旦您退出该方法,您就会丢失。该错误可能会在稍后处理但不会记录,并且您丢失了有关系统缺陷的信息。
            • 这是 Exception 类的 Data 属性很方便的地方——捕获所有本地信息以进行通用日志记录。这篇文章最初引起了我的注意:blog.abodit.com/2010/03/…
            【解决方案15】:

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

            【讨论】:

            • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
            • @HamzaLH,我同意这不是一个写得很好的答案,但它有信息,不同于其他答案和正面投票。所以我不明白,你为什么建议删除它? “针对主题并给出解决方案的简短答案仍然是答案。”来自meta.stackexchange.com/questions/226258/…
            • 这是仅链接的答案
            • 1.仅链接的答案应更改为 cmets,而不是删除。 2. 这是对其他 SO 问题的引用,而不是对外部站点的引用,外部站点被认为随着时间的推移不太可能被破坏。 3. 它有一些额外的描述,使它不是“仅链接” - 见meta.stackexchange.com/questions/225370/…
            【解决方案16】:

            大多数答案都在谈论场景 catch-log-rethrow。

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

            【讨论】:

            • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
            • @TonyDong,我同意这不是一个写得很好的答案,但它有信息,不同于其他答案和正面投票。所以我不明白,你为什么建议删除它?顺便说一句,5年后的链接仍然有效。 “针对主题并给出解决方案的简短答案仍然是答案。”来自meta.stackexchange.com/questions/226258/…
            • Stackoverflow 只有这个建议。
            • @TonyDong,如果答案不是绝对没用,你应该选择“看起来不错”
            【解决方案17】:

            当您没有特定代码来处理当前异常时,或者当您有处理特定错误情况的逻辑但想跳过所有其他情况时,通过 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 运行时不必重建异常对象 在重新扔之前。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-10-04
              • 2011-06-27
              • 2010-10-03
              • 1970-01-01
              • 2020-02-13
              • 1970-01-01
              相关资源
              最近更新 更多