【问题标题】:Why is try {...} finally {...} good; try {...} catch{} bad?为什么 try {...} finally {...} 好;尝试 {...} catch{} 不好?
【发布时间】:2010-09-12 19:56:20
【问题描述】:

我看到有人说不带参数使用 catch 是不好的形式,特别是如果那个 catch 没有做任何事情:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
catch   // No args, so it will catch any exception
{}
reader.Close();

但是,这被认为是好的形式:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
finally   // Will execute despite any exception
{
  reader.Close();
}

据我所知,将清理代码放在 finally 块中和将清理代码放在 try..catch 块之后的唯一区别是,如果你的 try 块中有 return 语句(在这种情况下,清理代码in finally 会运行,但是 try..catch 之后的代码不会)。

要不然finally有什么特别的?

【问题讨论】:

  • 在你试图抓住一只你无法应付的老虎之前,你应该记录下你最后的愿望。
  • Exceptions 文档中的主题可能会提供一些很好的见解。另请查看Finally Block 示例。

标签: c# .net exception-handling try-catch try-catch-finally


【解决方案1】:

如果您的方法知道如何在本地处理异常,请使用Try..Catch..Finally。异常发生在 Try 中,在 Catch 中处理,然后在 finally 中完成清理。

如果您的方法不知道如何处理异常,但一旦发生需要清理,请使用Try..Finally

如果在调用方法中有任何合适的 Catch 语句,异常就会传播到调用方法并进行处理。如果当前方法或任何调用方法中没有异常处理程序,则应用程序崩溃。

Try..Finally 确保在将异常传播到调用方法之前完成本地清理。

【讨论】:

  • 这个答案虽然基本,但绝对是最好的答案。养成 try/catch/finally 的习惯很好,即使后两者之一是空的。在非常罕见的情况下,catch 块可能存在并且为空,但至少如果您总是编写 try/catch/finally,您会在阅读代码时看到空块。有一个空的 finally 块也是有帮助的。如果您稍后需要清理,或者需要在异常时调试状态,这将非常有帮助。
【解决方案2】:

如果您阅读C# for programmers,您就会明白,finally 块旨在优化应用程序并防止内存泄漏。

CLR 并不能完全消除泄漏...如果程序无意中保留了对不需要的对象的引用,可能会发生内存泄漏

例如,当您打开文件或数据库连接时,您的机器将分配内存来满足该事务,并且除非执行了弃置或关闭命令,否则不会保留该内存。但如果在交易过程中发生错误,除非在try.. finally..块内,否则继续执行的命令将不会终止。

catchfinally 的不同之处在于,catch 旨在让您自行处理/管理或解释错误。把它想象成一个告诉你“嘿,我抓到了一些坏人,你想让我对他们做什么?”的人。 而finally 旨在确保您的资源被正确放置。想想一个人,不管有没有坏人,他都会确保你的财产仍然安全。

你应该让这两者永远合作。

例如:

try
{
  StreamReader reader=new  StreamReader("myfile.txt");
  //do other stuff
}
catch(Exception ex){
 // Create log, or show notification
 generic.Createlog("Error", ex.message);
}
finally   // Will execute despite any exception
{
  reader.Close();
}

【讨论】:

    【解决方案3】:

    添加一个 catch 子句只是为了重新抛出异常是不好的做法。

    【讨论】:

      【解决方案4】:

      无论如何,最终都会被执行。因此,如果您的 try 块成功,它将执行,如果您的 try 块失败,它将执行 catch 块,然后执行 finally 块。

      另外,最好尝试使用以下构造:

      using (StreamReader reader=new  StreamReader("myfile.txt"))
      {
      }
      

      由于 using 语句自动包装在 try / finally 中,流将自动关闭。 (如果您想实际捕获异常,则需要在 using 语句周围放置一个 try / catch)。

      【讨论】:

      • 这是不正确的。 using 不会用 try/catch 包装代码,应该说 try/finally
      【解决方案5】:

      try {...} catch{} 并不总是坏事。这不是一种常见的模式,但是当我无论如何都需要关闭资源时,我确实倾向于使用它,比如在线程结束时关闭(可能)打开的套接字。

      【讨论】:

        【解决方案6】:

        虽然以下 2 个代码块是等价的,但它们并不等价。

        try
        {
          int i = 1/0; 
        }
        catch
        {
          reader.Close();
          throw;
        }
        
        try
        {
          int i = 1/0;
        }
        finally
        {
          reader.Close();
        }
        
        1. 'finally' 是意图揭示代码。您向编译器和其他程序员声明此代码无论如何都需要运行。
        2. 如果你有多个 catch 块并且你有清理代码,你需要 finally。如果没有 finally,您将在每个 catch 块中复制您的清理代码。 (DRY 原则)

        finally 块是特殊的。 CLR 将 finally 块与 catch 块分开识别和处理代码,并且 CLR 竭尽全力保证 finally 块将始终执行。它不仅仅是来自编译器的语法糖。

        【讨论】:

          【解决方案7】:

          “最后”是“您必须始终做的事情以确保程序状态正常”的声明。因此,如果有任何异常可能会抛出程序状态,那么拥有一个总是好的形式。编译器还竭尽全力确保您的 finally 代码运行。

          “Catch”是“我可以从这个异常中恢复”的声明。您应该只从您真正可以纠正的异常中恢复 - 不带参数的 catch 说“嘿,我可以从任何事情中恢复!”,这几乎总是不正确的。

          如果可以从每个异常中恢复,那么这确实是一个语义上的狡辩,关于你声明你的意图是什么。但是,事实并非如此,而且几乎可以肯定,您的框架之上的框架将能够更好地处理某些异常。因此,使用 finally,让您的清理代码免费运行,但仍然让更多知识渊博的处理程序来处理问题。

          【讨论】:

          • 您的情绪很普遍,但不幸的是忽略了另一个重要案例:明确使不变量可能不再成立的对象无效。一种常见的模式是代码获取锁,对对象进行一些更改,然后释放锁。如果在进行部分但不是全部更改后发生异常,则对象可能会处于无效状态。尽管恕我直言,应该存在更好的替代方案,但我知道没有比捕获在对象状态可能无效时发生的任何异常、明确使状态无效并重新抛出更好的方法。
          【解决方案8】:

          我同意这里似乎达成的共识——空的“catch”是不好的,因为它掩盖了 try 块中可能发生的任何异常。

          另外,从可读性的角度来看,当我看到一个“try”块时,我假设会有一个相应的“catch”语句。如果您仅使用“尝试”以确保在“最终”块中取消分配资源,则可以考虑使用 'using' statement

          using (StreamReader reader = new StreamReader('myfile.txt'))
          {
              // do stuff here
          } // reader.dispose() is called automatically
          

          您可以对任何实现 IDisposable 的对象使用“using”语句。对象的 dispose() 方法在块结束时自动调用。

          【讨论】:

            【解决方案9】:

            只要不引发异常,您的示例之间的有效差异就可以忽略不计。

            但是,如果在 'try' 子句中抛出异常,第一个示例将完全吞下它。第二个示例将异常引发到调用堆栈的下一步,因此所述示例的不同之处在于,一个完全掩盖了任何异常(第一个示例),另一个(第二个示例)保留了异常信息以供以后可能处理,而仍在执行 'finally' 子句中的内容。

            例如,如果您将代码放在第一个引发异常的示例的“catch”子句中(无论是最初引发的异常,还是新异常),读取器清理代码将永远不会执行。最终执行无论在“catch”子句中发生什么。

            所以,'catch' 和 'finally' 之间的主要区别在于,'finally' 块的内容(除了少数罕见的例外)可以被视为 保证 执行,即使在面对意外的异常,而任何跟在 'catch' 子句之后的代码(但在 'finally' 子句之外)都不会有这样的保证。

            顺便提一下,Stream 和 StreamReader 都实现了 IDisposable,并且可以包装在“使用”块中。 'Using' 块是 try/finally 的语义等价物(没有 'catch'),因此您的示例可以更简洁地表示为:

            using (StreamReader reader = new  StreamReader("myfile.txt"))
            {
              int i = 5 / 0;
            }
            

            ...当 StreamReader 实例超出范围时,它将关闭并处理它。 希望这会有所帮助。

            【讨论】:

              【解决方案10】:

              首先,捕获您不想处理的异常是一种不好的做法。查看提高 .NET 应用程序性能和可扩展性中的Chapter 5 about .Net Performance。旁注,您可能应该在 try 块中加载流,这样,如果失败,您可以捕获相关的异常。在 try 块之外创建流违背了它的目的。

              【讨论】:

                【解决方案11】:

                try..finally 块仍会抛出任何引发的异常。 finally 所做的只是确保在抛出异常之前运行清理代码。

                带有空 catch 的 try..catch 将完全消耗任何异常并隐藏它发生的事实。读者将被关闭,但不知道是否发生了正确的事情。如果您的意图是将 i 写入文件怎么办?在这种情况下,您将无法访问该部分代码,并且 myfile.txt 将为空。所有下游方法都正确处理了吗?当您看到空文件时,您是否能够正确猜测它是空的,因为抛出了异常?最好抛出异常并让它知道你做错了什么。

                另一个原因是这样的 try..catch 是完全不正确的。你这样做的意思是,“无论发生什么,我都能应付。” StackOverflowException呢,那你能清理一下吗? OutOfMemoryException 呢?一般来说,您应该只处理您期望并知道如何处理的异常。

                【讨论】:

                  【解决方案12】:

                  捕获所有异常的 try/catch 块的问题是,如果发生未知异常,您的程序现在处于不确定状态。这完全违反了快速失败规则——如果发生异常,您不希望程序继续运行。上面的 try/catch 甚至会捕获 OutOfMemoryExceptions,但这绝对是你的程序不会运行的状态。

                  Try/finally 块允许您执行清理代码,同时仍然快速失败。在大多数情况下,您只想在全局级别捕获所有异常,以便记录它们,然后退出。

                  【讨论】:

                    【解决方案13】:

                    取自:here

                    引发和捕获异常不应作为成功执行方法的一部分而经常发生。在开发类库时,客户端代码必须有机会在执行可能导致引发异常的操作之前测试错误情况。例如,System.IO.FileStream 提供了一个 CanRead 属性,可以在调用 Read 方法之前对其进行检查,从而防止引发潜在的异常,如下面的代码 sn-p 所示:

                    Dim str As Stream = GetStream() 如果(str.CanRead)那么 '读取流的代码 结束如果

                    在调用可能引发异常的特定方法之前是否检查对象状态的决定取决于对象的预期状态。如果使用应该存在的文件路径和应该以读取模式返回文件的构造函数创建 FileStream 对象,则不需要检查 CanRead 属性;无法读取 FileStream 将违反所进行的方法调用的预期行为,并且应该引发异常。相反,如果方法被记录为返回可能或可能不可读的 FileStream 引用,建议在尝试读取数据之前检查 CanRead 属性。

                    为了说明使用“运行直到异常”编码技术可能导致的性能影响,将在转换失败时抛出 InvalidCastException 的转换的性能与 C# as 运算符进行比较,如果转换则返回空值失败。这两种技术的性能在强制转换有效的情况下是相同的(参见测试 8.05),但是对于强制转换无效并且使用强制转换导致异常的情况,使用强制转换比使用作为操作员(见测试 8.06)。抛出异常技术的高性能影响包括分配、抛出和捕获异常的成本以及异常对象后续垃圾回收的成本,这意味着抛出异常的瞬时影响并没有这么高。随着越来越多的异常被抛出,频繁的垃圾收集成为一个问题,因此频繁使用异常抛出编码技术的总体影响将类似于测试 8.05。

                    【讨论】:

                    • Scott——如果您在上面引用的文字在expertsexchange.com 的付费墙后面,您可能不应该在此处发布。我可能错了,但我敢打赌这不是一个好主意。
                    【解决方案14】:

                    可能有很多原因,异常执行起来很慢。如果这种情况经常发生,您很容易缩短执行时间。

                    【讨论】:

                      【解决方案15】:

                      Finally 是可选的——如果没有要清理的资源,就没有理由设置“Finally”块。

                      【讨论】:

                        【解决方案16】:

                        因为当那一行抛出异常时,你不会知道。

                        使用第一块代码,异常将被吸收,即使程序状态可能错误,程序仍将继续执行。

                        在第二个块中,异常将被抛出并冒泡reader.Close()仍然保证运行。

                        如果没有预料到异常,那么不要这样放置 try..catch 块,当程序进入错误状态并且您不知道原因时,以后将很难调试。

                        【讨论】:

                          【解决方案17】:

                          使用 finally,您可以清理资源,即使您的 catch 语句将异常抛出给调用程序。您的示例包含空的 catch 语句,几乎没有什么区别。但是,如果在你的 catch 中,你做了一些处理并抛出错误,或者甚至根本没有一个 catch,finally 仍然会运行。

                          【讨论】:

                            【解决方案18】:

                            从可读性的角度来看,它更明确地告诉未来的代码阅读者“这里的内容很重要,无论发生什么都需要完成。”这很好。

                            此外,空的 catch 语句往往对它们有某种“味道”。它们可能表明开发人员没有考虑可能发生的各种异常以及如何处理它们。

                            【讨论】:

                              【解决方案19】:

                              如果您不知道要捕获什么异常类型或如何处理它,那么使用 catch 语句是没有意义的。你应该把它留给可能有更多关于这种情况的信息的上级来电者知道该怎么做。

                              你应该仍然有一个 finally 语句以防出现异常,这样你就可以在异常被抛出给调用者之前清理资源。

                              【讨论】:

                                【解决方案20】:

                                最大的不同是try...catch 会吞下异常,隐藏发生错误的事实。 try..finally 将运行您的清理代码,然后异常将继续运行,由知道如何处理它的东西处理。

                                【讨论】:

                                • 任何考虑到封装的代码都可能只能在引发异常的地方处理异常。简单地把它传回调用堆栈,绝望地希望其他东西能够处理一些任意异常,这是灾难的根源。
                                • 在大多数情况下,为什么会在应用程序级别(例如,某个配置设置)发生特定异常比在类库级别更明显。
                                • David - 我希望程序快速失败,这样我才能意识到问题,而不是让程序在未知状态下运行。
                                • 如果您的程序在发生异常后处于未知状态,那么您的代码是错误的。
                                • @DavidArno,任何考虑到封装的代码都应该只处理其范围内的异常。其他任何事情都应该传递给其他人处理。如果我有一个应用程序从用户那里获取文件名然后读取文件,并且我的文件阅读器在打开文件时遇到异常,它应该将它们传递(或使用异常并抛出一个新异常),以便应用程序可以说,嘿 - 文件没有打开,让我们提示用户输入另一个文件。文件阅读器不应该能够提示用户或采取任何其他行动作为响应。它的唯一目的是读取文件。
                                猜你喜欢
                                • 1970-01-01
                                • 1970-01-01
                                • 2012-02-07
                                • 2010-11-12
                                • 2013-05-18
                                • 1970-01-01
                                • 2010-11-23
                                • 2010-10-01
                                • 1970-01-01
                                相关资源
                                最近更新 更多