如果 finally 块抛出异常会发生什么?

320

如果 finally 块抛出异常,会发生什么

具体而言,如果异常在 finally 块的中途被抛出,那么在此块中剩余的语句(即在异常被抛出之后的语句)是否会被调用?

我知道异常会向上级传播。


9
为什么不试一试呢?但在这种情况下,我最喜欢的是在最终块(finally block)之前返回,然后从最终块中返回其他内容。 :) - ANeves
11
finally块中的所有语句都必须执行,不能有return语句。http://msdn.microsoft.com/zh-cn/library/0hbbzekw(VS.80).aspx - Tim Scarborough
在 try 块中发生的原始异常已经丢失。 - masterxilo
11个回答

472
如果 finally 块抛出异常,会发生什么事情?这个异常会向外并向上传递,并且可以在更高层次上进行处理。在异常被抛出的地方之后,您的 finally 块将不会继续执行。如果 finally 块在处理早期异常时正在执行,则第一个异常会丢失。
C# 4 语言规范 §8.9.5:如果 finally 块抛出另一个异常,则当前异常的处理将被终止。

16
除非是ThreadAbortException,否则整个finally块将首先完成,因为它是一个关键部分。 - Dmytro Shevchenko
1
@Shedal - 你说得对,但这仅适用于“某些异步异常”,即ThreadAbortException。对于正常的单线程代码,我的答案是正确的。 - H H
@HenkHolterman:如果在写入流打开的情况下调用Dispose,并且数据无法写入,则使Dispose静默失败将是危险的,特别是如果在Dispose返回后没有异常挂起。不幸的是,Dispose无法知道它是否被调用于成功或失败的using块,因此无法避免在调用Dispose时破坏任何可能挂起的异常,或者在出现问题时掩盖危险的静默故障。 - supercat
1
@HenkHolterman:直接连接的主硬盘很少出现磁盘已满的错误,但程序有时会将文件写入可移动或网络磁盘;这些问题可能更为常见。如果有人在文件完全写入之前拔出USB存储设备,最好立即告诉他们,而不是等到他们到达目的地后发现文件已损坏。当存在早期错误时,让步于早期错误可能是明智的行为,但当不存在早期错误时,报告问题比不报告问题更好。 - supercat
这还正确吗?Microsoft的设计规则[CA1065](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1065)的文档指出:“从终结器中抛出异常会导致CLR快速失败,从而拆除进程。因此,应始终避免在终结器中抛出异常。” - andypea
显示剩余3条评论

116

对于这样的问题,我通常会在Visual Studio中打开一个空的控制台应用程序项目,并编写一小段示例程序:

using System;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            try
            {
                throw new Exception("exception thrown from try block");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Inner catch block handling {0}.", ex.Message);
                throw;
            }
            finally
            {
                Console.WriteLine("Inner finally block");
                throw new Exception("exception thrown from finally block");
                Console.WriteLine("This line is never reached");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Outer catch block handling {0}.", ex.Message);
        }
        finally
        {
            Console.WriteLine("Outer finally block");
        }
    }
}

当你运行程序时,你会看到 catchfinally 块被执行的确切顺序。请注意,在抛出异常后,finally 块中的代码不会被执行(实际上,在这个示例程序中,Visual Studio 甚至会警告你它已检测到无法访问的代码):

内部 catch 块处理从 try 块抛出的异常。
内部 finally 块
外部 catch 块处理从 finally 块抛出的异常。
外部 finally 块

额外说明

正如 Michael Damatov 指出的那样,如果你不在(内部)catch块中处理try块中的异常,那么该异常就会被“吃掉”。事实上,在上面的示例中,重新抛出的异常不会出现在外部catch块中。为了更清楚地说明这一点,请看以下稍作修改的示例:

using System;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            try
            {
                throw new Exception("exception thrown from try block");
            }
            finally
            {
                Console.WriteLine("Inner finally block");
                throw new Exception("exception thrown from finally block");
                Console.WriteLine("This line is never reached");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Outer catch block handling {0}.", ex.Message);
        }
        finally
        {
            Console.WriteLine("Outer finally block");
        }
    }
}

从输出结果可以看出,内部异常被“丢失”(即被忽略):

内部 finally 块
由 finally 块抛出的异常被外部 catch 块捕获。
外部 finally 块

2
由于在内部catch中抛出了异常,因此在此示例中将永远不会到达“内部finally块”。 - Theofanis Pantelides
5
@Theofanis Pantelides:不,finally 块几乎总是会被执行的,包括这个例子中的内部 finally 块(你可以自己试试这个示例程序)。但如果出现无法恢复的异常(例如 EngineExecutionException),则 finally 块将不会被执行,但在这种情况下,您的程序无论如何都会立即终止。 - Dirk Vollmar
1
我不明白你第一段代码中第一个catch语句中throw的作用是什么。我尝试在控制台应用程序中使用它和不使用它,但没有发现任何区别。 - JohnPan
@johnpan:重点是要表明finally块始终会执行,即使try和catch块都抛出异常。实际上,控制台输出没有任何区别。 - Dirk Vollmar

11

如果存在待处理的异常(当try块有finally但没有catch时),新的异常将替换原有异常。

如果不存在待处理的异常,它就像在finally块外抛出异常一样运行。


如果有一个匹配的catch块(重新)抛出异常,那么异常也可能会挂起。 - stakx - no longer contributing

7

如果原始异常对您更重要,那么可以使用以下快速(且相当明显)的代码片段来保存“原始异常”(在try块中抛出),并且放弃“finally异常”(在finally块中抛出):

try
{
    throw new Exception("Original Exception");
}
finally
{
    try
    {
        throw new Exception("Finally Exception");
    }
    catch
    { }
}

当执行上述代码时,“原始异常”会向上传播到调用堆栈,并且“最终异常”会丢失。

5
异常将被传播。

2
@bitbonk:像往常一样,从内到外。 - Piskvor left the building

3

我不得不这样做来捕获由于异常而未打开流而尝试关闭流时出现的错误。

errorMessage = string.Empty;

try
{
    byte[] requestBytes = System.Text.Encoding.ASCII.GetBytes(xmlFileContent);

    webRequest = WebRequest.Create(url);
    webRequest.Method = "POST";
    webRequest.ContentType = "text/xml;charset=utf-8";
    webRequest.ContentLength = requestBytes.Length;

    //send the request
    using (var sw = webRequest.GetRequestStream()) 
    {
        sw.Write(requestBytes, 0, requestBytes.Length);
    }

    //get the response
    webResponse = webRequest.GetResponse();
    using (var sr = new StreamReader(webResponse.GetResponseStream()))
    {
        returnVal = sr.ReadToEnd();
        sr.Close();
    }
}
catch (Exception ex)
{
    errorMessage = ex.ToString();
}
finally
{
    try
    {
        if (webRequest.GetRequestStream() != null)
            webRequest.GetRequestStream().Close();
        if (webResponse.GetResponseStream() != null)
            webResponse.GetResponseStream().Close();
    }
    catch (Exception exw)
    {
        errorMessage = exw.ToString();
    }
}

如果webRequest已经创建,但在连接期间出现连接错误,则会发生以下情况:
using (var sw = webRequest.GetRequestStream())

那么finally语句块会捕获异常,试图关闭它认为已经打开的连接,因为webRequest已经被创建。

如果finally语句块内没有try-catch语句,这段代码将在清理webRequest时引发未处理的异常。

if (webRequest.GetRequestStream() != null) 

代码从那里退出时,没有正确处理发生的错误,因此会对调用方法造成问题。

希望这个例子能够帮助你。


3
异常会一直传递到更高层次,应该在更高的级别上处理异常。如果异常没有在更高层次上处理,应用程序就会崩溃。无论是否出现异常,“finally”块都保证会执行。如果在try块中抛出异常,“finally”块的执行会在此处停止。如果“finally”块在发生异常时被执行,并且该异常未被处理,那么原始的异常将会丢失。
public class Exception
{
    public static void Main()
    {
        try
        {
            SomeMethod();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }

    public static void SomeMethod()
    {
        try
        {
            // This exception will be lost
            throw new Exception("Exception in try block");
        }
        finally
        {
            throw new Exception("Exception in finally block");
        }
    }
} 

详细信息请查看这篇精彩文章


2

在另一个异常仍然存在时抛出异常,会导致第一个异常被第二个(后面的)异常替换。

以下是演示这种情况的代码:

    public static void Main(string[] args)
    {
        try
        {
            try
            {
                throw new Exception("first exception");
            }
            finally
            {
                //try
                {
                    throw new Exception("second exception");
                }
                //catch (Exception)
                {
                    //throw;
                }
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
  • 运行代码,你将看到“第二个异常”
  • 取消注释try和catch语句,你将看到“第一个异常”
  • 同样取消注释throw;语句,你将再次看到“第二个异常”。

值得注意的是,清理“严重”异常的过程可能会抛出一个异常,该异常只能在特定代码块之外被捕获。使用异常筛选器(在vb.net中可用,但不适用于C#),可以检测到此条件。虽然代码无法做太多事情来“处理”它,但如果使用任何类型的日志记录框架,则几乎肯定值得记录。 C++的方法是在清理过程中发生异常时触发系统崩溃,这很丑陋,但让异常消失则更加令人难以接受。 - supercat

2

几个月前,我也遇到了类似的问题。

    private  void RaiseException(String errorMessage)
    {
        throw new Exception(errorMessage);
    }

    private  void DoTaskForFinally()
    {
        RaiseException("Error for finally");
    }

    private  void DoTaskForCatch()
    {
        RaiseException("Error for catch");
    }

    private  void DoTaskForTry()
    {
        RaiseException("Error for try");
    }


        try
        {
            /*lacks the exception*/
            DoTaskForTry();
        }
        catch (Exception exception)
        {
            /*lacks the exception*/
            DoTaskForCatch();
        }
        finally
        {
            /*the result exception*/
            DoTaskForFinally();
        }

为了解决这个问题,我制作了一个实用类,例如:
class ProcessHandler : Exception
{
    private enum ProcessType
    {
        Try,
        Catch,
        Finally,
    }

    private Boolean _hasException;
    private Boolean _hasTryException;
    private Boolean _hasCatchException;
    private Boolean _hasFinnallyException;

    public Boolean HasException { get { return _hasException; } }
    public Boolean HasTryException { get { return _hasTryException; } }
    public Boolean HasCatchException { get { return _hasCatchException; } }
    public Boolean HasFinnallyException { get { return _hasFinnallyException; } }
    public Dictionary<String, Exception> Exceptions { get; private set; } 

    public readonly Action TryAction;
    public readonly Action CatchAction;
    public readonly Action FinallyAction;

    public ProcessHandler(Action tryAction = null, Action catchAction = null, Action finallyAction = null)
    {

        TryAction = tryAction;
        CatchAction = catchAction;
        FinallyAction = finallyAction;

        _hasException = false;
        _hasTryException = false;
        _hasCatchException = false;
        _hasFinnallyException = false;
        Exceptions = new Dictionary<string, Exception>();
    }


    private void Invoke(Action action, ref Boolean isError, ProcessType processType)
    {
        try
        {
            action.Invoke();
        }
        catch (Exception exception)
        {
            _hasException = true;
            isError = true;
            Exceptions.Add(processType.ToString(), exception);
        }
    }

    private void InvokeTryAction()
    {
        if (TryAction == null)
        {
            return;
        }
        Invoke(TryAction, ref _hasTryException, ProcessType.Try);
    }

    private void InvokeCatchAction()
    {
        if (CatchAction == null)
        {
            return;
        }
        Invoke(TryAction, ref _hasCatchException, ProcessType.Catch);
    }

    private void InvokeFinallyAction()
    {
        if (FinallyAction == null)
        {
            return;
        }
        Invoke(TryAction, ref _hasFinnallyException, ProcessType.Finally);
    }

    public void InvokeActions()
    {
        InvokeTryAction();
        if (HasTryException)
        {
            InvokeCatchAction();
        }
        InvokeFinallyAction();

        if (HasException)
        {
            throw this;
        }
    }
}

可以像这样使用

try
{
    ProcessHandler handler = new ProcessHandler(DoTaskForTry, DoTaskForCatch, DoTaskForFinally);
    handler.InvokeActions();
}
catch (Exception exception)
{
    var processError = exception as ProcessHandler;
    /*this exception contains all exceptions*/
    throw new Exception("Error to Process Actions", exception);
}

但如果你想要使用参数和返回类型,那就是另外一回事了。


0
public void MyMethod()
{
   try
   {
   }
   catch{}
   finally
   {
      CodeA
   }
   CodeB
}

处理 CodeA 和 CodeB 抛出的异常的方式是相同的。

finally 块中抛出的异常没有什么特殊之处,将其视为代码 B 抛出的异常即可。


你能详细说明一下吗?你说的异常是指相同的异常吗? - Dirk Vollmar

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