ChatGPT解决这个技术问题 Extra ChatGPT

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

我正在查看关于可序列化 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++。

Stack Overflow 问题 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 一样)。

参考:

MSDN - 异常处理

MSDN - try-catch(C# 参考)

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

R
Richard Szalay

第一的;文章中的代码这样做的方式是邪恶的。 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 不会重新启动堆栈跟踪。
G
Geoff

不要这样做,

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。
A
Abel

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
}

戴夫,但是(至少在 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 中即将出现过滤异常!
P
Peter Mortensen

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

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

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

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


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

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

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

谢谢,是的,异常包装(尤其是链式)是完全理智的……不理智的是捕获异常只是为了让您可以丢弃堆栈跟踪,或者更糟的是,吃掉它。
A
Arjan Einbu

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

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


P
Peter Mortensen

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

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


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

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

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

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


s
supercat

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

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

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


s
statler

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

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

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


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

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

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

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


S
Sheff

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

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

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


佚名

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

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


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

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


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

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

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


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

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