在catch/finally块中吞咽异常抛出

11

通常我会遇到这样的情况:在catch/finally块中,必须捕获清理代码抛出的异常,以防止原始异常被吞噬。

例如:

// Closing a file in Java
public void example1() throws IOException {
    boolean exceptionThrown = false;
    FileWriter out = new FileWriter(“test.txt”);
    try {
        out.write(“example”);
    } catch (IOException ex) {
        exceptionThrown = true;
        throw ex;
    } finally {
        try {
            out.close();
        } catch (IOException ex) {
            if (!exceptionThrown) throw ex;
            // Else, swallow the exception thrown by the close() method
            // to prevent the original being swallowed.
        }
    }
}

// Rolling back a transaction in .Net
public void example2() {
    using (SqlConnection connection = new SqlConnection(this.connectionString)) {
        SqlCommand command = connection.CreateCommand();
        SqlTransaction transaction = command.BeginTransaction();
        try {
            // Execute some database statements.
            transaction.Commit();
        } catch {
            try {
                transaction.Rollback();
            } catch {
                // Swallow the exception thrown by the Rollback() method
                // to prevent the original being swallowed.
            }
            throw;
        }
    }
}

假设在方法块的范围内记录任何异常不是选项,但将由调用example1()example2()方法的代码执行。

吞噬close()Rollback()方法抛出的异常是一个好主意吗?如果不是,有什么更好的处理上述情况的方式,以便异常不被忽略?


我使用的模式与你的“example1”几乎完全相同。 - Douglas
对于实现 IDisposable 接口的资源(通常情况下),我已经实现了一个扩展方法,将您的 example1 的逻辑与其他策略结合起来。 - Douglas
8个回答

25

我不喜欢捕获并重新抛出异常。

如果你捕获了它,请对它做一些处理,哪怕只是记录异常信息。

如果你无法处理它,请不要捕获它 - 在方法签名中添加 throws 子句。

捕获异常告诉我,要么你能处理特殊情况并有恢复计划,要么“责任到此为止”,因为异常不能以原来的形式传播(例如,没有堆栈跟踪返回给用户)。


2
同意。不要考虑如何结束通话,而是考虑向那些必须根据日志来修复晦涩难懂且无法重现的错误的可怜开发人员提供相关的信息。 - Thorbjørn Ravn Andersen
5
不同意 - 有时“对其进行操作”就是重新抛出它 - 特别是捕获已检查异常但将更有意义的未检查异常传递给客户端代码(我认为这种情况仅适用于Java)。Spring的JdbcTemplate是一个很好的例子,它可以捕获已检查异常并将其作为更有意义的未检查异常公开。 - MetroidFan2002
@MetroidFan2002 - 我认为他的意思是捕获并重新抛出相同的异常对象,而不是抛出一个新的异常。 - Ken Liu
1
@Metroid,我认为你和Ken总结了“责任在此”的一个情况。如果我将其捕获并作为更有意义和/或未经检查的异常重新抛出,那么我就是在说原始异常不能传播到应用程序的那个层之外。 - duffymo
@duffymo 这要看情况。如果由于某种原因您希望对象记住尝试做某事失败的次数,那么您需要捕获异常,将计数变量加1,然后再抛出异常。 - Crono
显示剩余2条评论

7

您可以创建一个自定义的Exception类型,该类型可以包含两个异常。如果您重载ToString(),您可以记录两个异常。

try
{
    transaction.Commit();
}
catch(Exception initialException)
{
    try
    {
        transaction.Rollback();
    }
    catch(Exception rollbackException)
    {
        throw new RollbackException(initialException, rollbackException);
    }

    throw;
}

1
回滚的重点在于提交时发生了一些错误,你需要使用回滚来处理它。有人可能会认为第一个异常已经被回滚处理了,因此你不需要初始异常。如果回滚成功,你就看不到第一个异常。只有当回滚失败时,你才会看到两个异常。总之,我同意新的异常类型。如果你想看到多个异常,请捕获它们并放入一个新的异常中。这是个好主意。 - shimpossible

4

这就是为什么Commons IO有一个IOUtils.closeQuietly方法。在大多数情况下,文件关闭时出现问题并不那么重要。

必须回滚的数据库事务可能更有趣,因为在这种情况下函数没有执行它应该执行的操作(将内容放入数据库中)。


1

在 C# 代码中没有回滚事务的理由。如果你在不回滚(或提交)事务的情况下关闭连接,那么这是等效且更有效的操作...

public void example2() {
  using (SqlConnection connection = new SqlConnection(this.connectionString))
  using (SqlCommand command = connection.CreateCommand())
  using (SqlTransaction transaction = command.BeginTransaction()) {
    // Execute some database statements.
    transaction.Commit();
  }
}

然后你就完成了。

使用using语句可以确保(通过finally)无论发生任何异常,连接都会关闭,并让原始异常冒出(带有完整/正确的堆栈跟踪)。如果在调用Commit之前发生异常,则事务永远不会提交,并且在事务/连接关闭时将自动回滚。


0
我会考虑将example1重写如下:
// Closing a file in Java
public void example1() throws IOException {
    boolean success = false;
    FileWriter out = new FileWriter(“test.txt”);
    try {
        out.write(“example”);
        success = true;
        out.close();
    } finally {
        if (!success) {
            try {
                out.close();
            } catch (IOException ex) {
                // log and swallow.
            }
        }
    }
}

success = true;移动到out.close();语句之后会使success的含义更清晰...尽管这可能导致out.close()被调用两次。

0

我认为异常应该是你不期望发生的事情。如果你预料到会出现异常,那么你应该对其进行处理。因此,在你的第一个例子中,如果你已经声明了方法将抛出 IOException,那么你可能不需要捕获 IOException。


0

如果不了解您的具体情况,您可以考虑抛出一个新的异常。在C#中,当抛出一个新的异常时,其中一个可选的构造函数接受一个现有的异常作为参数。例如:

throw new Exception("This is my new exception.", ex);

这样做的目的是保留原始异常。

另一个选项可能是使用 try .. catch .. finally 结构。

try { // 可能会抛出异常的正常代码 } catch (Exception ex) { // 处理第一个异常 } finally { // 处理任何清理工作,无论是否抛出异常 }

一般来说,如果我的代码可以在特定的 try .. catch 中处理异常,那么我不会重新抛出该异常。如果某个调用堆栈中的其他部分需要该异常,则会引发新异常并将原始异常设置为内部异常。


-2
通常,具有风险的代码会被放置在一个try-catch块中。所有嵌套的try-catch块都不是一个很好的想法,在我看来(或者尽可能避免嵌套的try-catch块,除非你真的需要它们)。
因为具有风险的代码是异常情况,所以将异常代码放在更特殊的情况中,这是很多不必要的工作。
例如,在example1()中,将所有具有风险的代码放在一个try-catch块中:
try{
FileWriter out = new FileWriter(“test.txt”);
out.write(“example”);
out.close();
} catch(Exception e) {
    //handle exception
}

或者,另一个好主意是为相同的 try 放置几个 catch(s):

try{
    FileWriter out = new FileWriter(“test.txt”);
    out.write(“example”);
    out.close();
} catch(FileNotFoundException e) {
        //if IOException, do this..
} catch(IOException e) {
        //if FileNotFound, do this..
} catch(Exception e) {
        //completely general exception, do this..
}

这段代码可能会泄漏文件句柄。如果 write 抛出异常,则 close 将不会被调用。 - McDowell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接