关于异常处理:为什么空catch会阻止一个坏主意?

Why are empty catch blocks a bad idea?

我刚刚看到一个关于try catch的问题,哪些人(包括jon skeet)认为空catch块是一个非常糟糕的主意?为什么会这样?在任何情况下,空捕获都不是错误的设计决策吗?

我的意思是,例如,有时候你想从某个地方(webservice,database)获得一些额外的信息,你真的不在乎是否会得到这些信息。所以你试着得到它,如果发生了什么,没关系,我就加一个"catch(exception-ignored)"就行了。


通常,空的Try-Catch是一个坏主意,因为您正在静默地吞咽错误条件,然后继续执行。有时这可能是正确的做法,但通常这是一个迹象,表明开发人员看到了一个异常,不知道如何处理它,因此使用了一个空的catch来解决问题。

这相当于在发动机警告灯上贴上黑带。

我相信如何处理异常取决于您所使用的软件的哪一层:雨林中的异常。


一般来说,它们是一个坏主意,因为它是一种真正罕见的情况,在这种情况下,一个故障(更一般地说,是异常情况)被正确地满足而没有任何响应。除此之外,空的catch块是使用异常引擎进行错误检查的人所使用的一个常用工具,他们应该先发制人。

说它总是坏的是不真实的……这是真的,很少。在某些情况下,您可能不在乎是否有错误,或者错误的存在以某种方式表明您无论如何都不能做任何事情(例如,在将以前的错误写入文本日志文件时,您得到一个IOException,这意味着您无论如何也不能写出新的错误)。


我不会说谁使用空的catch块是一个糟糕的程序员,不知道他在做什么……

如有必要,我使用空的挡块。有时我正在消费的库的程序员不知道他在做什么,甚至在没有人需要的情况下抛出异常。

例如,考虑一些HTTP服务器库,我不在乎服务器是否抛出异常,因为客户端已断开连接,无法发送index.html


在少数情况下,它是合理的。在python中,您经常看到这种构造:

1
2
3
4
try:
    result = foo()
except ValueError:
    result = None

因此,可以(取决于您的应用程序)执行以下操作:

1
2
3
4
5
6
7
result = bar()
if result == None:
    try:
        result = foo()
    except ValueError:
        pass # Python pass is equivalent to { } in curly-brace languages
 # Now result == None if bar() returned None *and* foo() failed

在最近的一个.NET项目中,我不得不编写代码来枚举插件DLL以查找实现某个接口的类。相关代码位(在vb.net中,对不起)是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    For Each dllFile As String In dllFiles
        Try
            ' Try to load the DLL as a .NET Assembly
            Dim dll As Assembly = Assembly.LoadFile(dllFile)
            ' Loop through the classes in the DLL
            For Each cls As Type In dll.GetExportedTypes()
                ' Does this class implement the interface?
                If interfaceType.IsAssignableFrom(cls) Then

                    ' ... more code here ...

                End If
            Next
        Catch ex As Exception
            ' Unable to load the Assembly or enumerate types -- just ignore
        End Try
    Next

尽管在这种情况下,我承认将失败记录到某个地方可能是一个改进。


只有在确实存在异常的情况下才应该抛出异常——一些超出规范的事情。一个空的catch块基本上说"发生了一些不好的事情,但我不在乎"。这是个坏主意。

如果您不想处理异常,就让它向上传播,直到到达可以处理它的代码为止。如果没有任何东西可以处理这个异常,那么它应该将应用程序取下。


我认为,如果你捕捉到一种特殊的异常类型,你知道它只会因为一个特殊的原因而被引发,那就没关系了,你期待着这个异常,并且真的不需要做任何事情。

但即使在这种情况下,调试消息也可能是有序的。


空的catch块通常被放入,因为编码人员并不真正知道它们在做什么。在我的组织中,一个空的catch块必须包含一个注释,说明为什么对异常不做任何事情是一个好主意。

在相关的注释中,大多数人不知道try块后面可以跟着catch或finally,只需要一个。


Per Josh Bloch -第65项:不要忽视有效Java的异常:

  • 空的catch块会破坏异常的目的。
  • 至少,catch块应该包含一个注释,解释为什么忽略异常是合适的。

  • 一个空的catch块本质上是说"我不想知道抛出了什么错误,我只想忽略它们。"

    它类似于vb6的On Error Resume Next,只是在抛出异常之后,try块中的任何内容都将被跳过。

    如果有什么东西坏了也没用。


    这与"不要使用异常来控制程序流"和"只在异常情况下使用异常"密切相关。如果这样做了,那么只有在出现问题时才会出现异常。如果有问题的话,你不想一声不响地失败。在不需要处理问题的罕见异常中,您至少应该记录异常,以防异常不再是异常。唯一比失败更糟糕的是默默地失败。


    我认为完全空的catch块是一个坏主意,因为无法推断忽略异常是代码的预期行为。在某些情况下,接受异常并返回false、null或其他值并不一定是坏事。.NET框架有许多这样的"尝试"方法。根据经验,如果您吞咽了一个异常,那么如果应用程序支持日志记录,就添加一条注释和一条日志语句。


    因为如果抛出一个异常,你将永远看不到它——默默地失败是最糟糕的选择——你将得到错误的行为,不知道它发生在哪里。至少在那里放一条日志信息!即使是"永远不会发生"的事情!


    空的catch块表示程序员不知道如何处理异常。它们正在抑制异常,以防冒泡,并由另一个try块正确处理。总是试着做一些你能抓住的事情。


    我发现空的catch语句最让人恼火的是其他程序员做的。我的意思是,当您需要从其他人调试代码时,任何空的catch语句都会使这样的任务比需要的任务更困难。imho catch语句应该始终显示某种类型的错误消息-即使没有处理错误,它也应该至少检测到它(仅在调试模式下才启用alt.on)


    一般来说,您应该只捕获可以实际处理的异常。这意味着在捕获异常时尽可能具体。捕获所有异常很少是一个好主意,忽略所有异常几乎总是一个非常坏的主意。

    我只能想到几个空catch块有一些有意义的用途的实例。如果您捕获的任何特定异常只是通过重新尝试操作来"处理",则不需要在catch块中执行任何操作。然而,记录异常发生的事实仍然是一个好主意。

    另一个示例:clr 2.0更改了处理终结器线程上未处理的异常的方式。在2.0之前,该过程可以在这种情况下生存下来。在当前的CLR中,如果终结器线程上出现未处理的异常,则进程将终止。

    请记住,只有在确实需要终结器的情况下才应该实现终结器,即使这样,在终结器中也应该尽可能少地实现一个终结器。但是,如果你的定稿人必须做的任何工作都可能引发一个例外,你需要在两种邪恶中取其一。是否由于未处理的异常而关闭应用程序?或者您想在或多或少未定义的状态下继续?至少在理论上,后者在某些情况下可能是两种邪恶中的较小者。在这种情况下,空catch块将阻止进程终止。


    I mean, for instance, sometimes you want to get some additional info from somewhere (webservice, database) and you really don't care if you'll get this info or not. So you try to get it, and if anything happens, that's ok, I'll just add a"catch (Exception ignored) {}" and that's all

    所以,以你的例子来说,在这种情况下这是一个坏主意,因为你正在捕获并忽略所有的异常。如果你只抓到EInfoFromIrrelevantSourceNotAvailable,而忽略了它,那没关系,但你没有。你也忽略了ENetworkIsDown,这可能重要,也可能不重要。你忽略了ENetworkCardHasMeltedEFPUHasDecidedThatOnePlusOneIsSeventeen,这几乎肯定是很重要的。

    如果将空catch块设置为只捕获(并忽略)某些类型的异常(您知道这些异常不重要),则它不是问题。在这种情况下,最好抑制并静默地忽略所有异常,而不先停下来检查它们,看它们是否是预期的/正常的/不相关的,这种情况非常罕见。


    在某些情况下,您可能会使用它们,但它们应该是非常罕见的。我可能使用其中一个的情况包括:

    • 异常日志记录;根据上下文,您可能希望发布未处理的异常或消息。

    • 循环的技术情况,如渲染、声音处理或列表框回调,在这些情况下,行为本身将证明问题,引发异常将妨碍解决,记录异常可能只会导致1000条"失败到XXX"的消息。

    • 不能失败的程序,尽管它们至少应该记录一些东西。

    对于大多数WinForms应用程序,我发现对于每个用户输入只有一条try语句就足够了。我使用以下方法:(AlertBox只是一个快速消息框。显示包装)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
      public static bool TryAction(Action pAction)
      {
         try { pAction(); return true; }
         catch (Exception exception)
         {
            LogException(exception);
            return false;
         }
      }

      public static bool TryActionQuietly(Action pAction)
      {
         try { pAction(); return true; }
         catch(Exception exception)
         {
            LogExceptionQuietly(exception);
            return false;
         }
      }

      public static void LogException(Exception pException)
      {
         try
         {
            AlertBox(pException, true);
            LogExceptionQuietly(pException);
         }
         catch { }
      }

      public static void LogExceptionQuietly(Exception pException)
      {
         try { Debug.WriteLine("Exception: {0}", pException.Message); } catch { }
      }

    然后每个事件处理程序都可以执行如下操作:

    1
    2
    3
    4
      private void mCloseToolStripMenuItem_Click(object pSender, EventArgs pEventArgs)
      {
         EditorDefines.TryAction(Dispose);
      }

    1
    2
    3
    4
      private void MainForm_Paint(object pSender, PaintEventArgs pEventArgs)
      {
         EditorDefines.TryActionQuietly(() => Render(pEventArgs));
      }

    理论上,您可以静默地尝试,这对于呈现调用可能更好,这样异常就不会生成无休止的消息。


    这可能永远都不是正确的事情,因为你在默默地传递每一个可能的例外。如果有一个特定的异常你期待,那么你应该测试它,如果它不是你的异常,再告诉你。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    try
    {
        // Do some processing.
    }
    catch (FileNotFound fnf)
    {
        HandleFileNotFound(fnf);
    }
    catch (Exception e)
    {
        if (!IsGenericButExpected(e))
            throw;
    }

    public bool IsGenericButExpected(Exception exception)
    {
        var expected = false;
        if (exception.Message =="some expected message")
        {
            // Handle gracefully ... ie. log or something.
            expected = true;
        }

        return expected;
    }


    你不应该有一个空的catch块。这就像隐藏了一个你知道的错误。至少您应该在日志文件中写一个异常,以便以后在需要时查看。


    如果您不知道在catch块中要做什么,可以只记录这个异常,但不要将其留空。

    1
    2
    3
    4
    5
    6
    7
    8
    9
            try
            {
                string a ="125";
                int b = int.Parse(a);
            }
            catch (Exception ex)
            {
                Log.LogError(ex);
            }