为什么 'continue' 语句不能在 'finally' 块中使用?

108

我没有问题,只是好奇。想象以下情况:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

由于出现编译错误 CS0157,因此此代码无法编译:

控件不能离开finally子句的主体

为什么?


7
所以,只是好奇。如果你完全明白为什么它编译不了,为什么还想要别人解释已经很明显的事情呢? =) - J. Steen
14
为什么在finally块中需要使用continue;?这不与try-catch块后面的continue;相同吗? - bansi
6
不,我确实知道 finally/continue 是 C# 编译器的限制 :-) 我对这种限制的原因也感到好奇。 - xanatos
5
从语言规范上说,这是基础CIL的限制。根据语言规范:“除非通过异常处理机制,否则永远不允许进行控制转移以进入catch处理程序或finally子句。”和“只有通过异常指令(leave、end.filter、end.catch或end.finally)才允许从受保护区域中进行控制转移。”br分支指令系列无法完成此操作。 - Unsigned
1
@Unsigned:这也是一个很好的限制,允许它会很奇怪 :) - user7116
显示剩余8条评论
11个回答

152
finally 块无论是否抛出异常,都会执行。如果抛出异常,那么 continue 会发生什么?你不能继续执行循环,因为未捕获的异常将转移控制权到另一个函数。
即使没有抛出异常,finally 在 try/catch 块内运行其他控制传输语句时也会运行,例如 return,这带来了相同的问题。
简而言之,根据 finally 的语义,不允许从 finally 块内部转移控制到其外部是没有意义的。
使用一些替代语义来支持这一点可能会更加混乱而不是有帮助,因为有简单的解决方法可以使所需的行为更加清晰。所以你会得到一个错误,并被迫认真思考你的问题。这是在 C# 中普遍采用的“将你扔进成功坑中”的想法。
如果你想忽略异常(通常情况下这是一个坏主意)并继续执行循环,请使用一个 catch-all 块:
foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

如果你想在没有未捕获异常时继续进行,只需将continue放在try块之外。


13
我会接受这个答案,因为您让我理解了微软可能决定不接受在finally中使用continue语句的原因。也许那些图像也让我信服了 :) - lpaloub
20
你能详细说明一下"把你扔进成功的深渊"这个想法吗?我没看懂 :-D - Ant
9
@Ant http://www.codinghorror.com/blog/2007/08/falling-into-the-pit-of-success.html - Josh
13
顺便提一下,这张图片是由Jon Skeet制作的,我从这里得到的:http://msmvps.com/blogs/jon_skeet/archive/2010/09/02/presentation-preparation.aspx - R. Martinho Fernandes
1
不错的回答。@lpaloub: 确实,这正是语言设计团队在制定此类限制时考虑到的问题。我注意到,在类似的原因下,yieldawaittry块之间也存在类似的限制。 - Eric Lippert
显示剩余3条评论

32

这里有一个可靠的来源:

continue语句不能退出finally块(第8.10节)。当在finally块中出现continue语句时,continue语句的目标必须在同一个finally块内;否则,将出现编译时错误。

这段内容摘自MSDN 的 8.9.2 The continue statement

文档说:

当控制权离开try语句时,finally块的语句总是会执行。无论控制转移是否因正常执行、由于执行break、continue、goto或return语句或由于将异常传播到try语句之外而发生。如果在执行finally块期间抛出异常,则将该异常传播到下一个封闭的try语句。如果另一个异常正在传播过程中,则该异常将丢失。异常传播过程在throw语句(第8.9.5节)的描述中进一步讨论。

内容来自 8.10 The try statement


31

你可能认为它有道理,但实际上没有道理

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

当抛出异常时,你打算使用 break 还是 continue?C# 编译器小组不希望自行决定使用 break 还是 continue。相反,他们决定抱怨开发人员的情况将会不明确,因为从 finally块 转移控制。

因此,开发人员的工作是清楚地说明他想要做什么,而不是编译器假设其他内容。

我希望你能理解为什么这不能编译!


相关的是,即使没有“catch”,在finally语句后面执行的路径也会受到catch是否通过异常退出的影响,并且没有机制可以说明它应该如何与continue交互。我喜欢你的例子,因为它展示了一个更大的问题。 - supercat
@supercat 我同意你的观点,我的回答展示了编译器面临的一个模糊情况的例子,但这种方法仍然存在许多问题。 - Sriram Sakthivel

16

正如其他人所说,但是重点放在异常处理上,这实际上是关于传递控制的模糊处理。

在您心中,您可能会想到以下场景:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

理论上,你可以追踪控制流并说:“是的,这是‘好’的”。没有抛出异常,也没有转移控制。但 C# 语言设计者考虑到了其他问题。

抛出异常

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

可怕的Goto语句

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

回归

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

分手

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

因此,总的来说,是的,在控制流没有被转移的情况下使用continue存在轻微的可能性,但很多(大部分?)情况涉及到异常或者return块。语言设计者认为这会过于模糊不清,并且(很可能)无法在编译时确保你的continue仅在控制流未被转移的情况下使用。

在使用Proguard进行混淆时,我也遇到了Java中相同的情况。您的解释非常好。C#会在编译时报错 - 完全合理。 - Dev_Vikram

11
通常情况下,在 finally 块中使用 continue 是没有意义的。看一下这个例子:
foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}

通常情况下,很多事情都没有意义。但这并不意味着它在特定情况下不能工作。例如:continue语句在循环语句之外没有意义,但这并不意味着它不被支持。 - zerkms
我认为这有些意义。item可以是文件,读取失败-> finally关闭文件。continue阻止其余的处理。 - jnovacho

5

"这个代码无法编译,但我认为它是完全有道理的"

我觉得并不是这样。

当你使用 catch(Exception) 的时候,你就不需要 finally (甚至可能不需要 continue)。

当你使用更现实的 catch(SomeException) 时,如果一个异常没有被捕获,那么应该发生什么?你的continue想要走一条路,而异常处理则另外一条路。


2
我认为当你使用catch时,你可能需要使用finally。它通常用于可靠地关闭资源。 - jnovacho
不仅仅是异常。你可以在trycatch块中轻松地使用return语句。现在我们该怎么办?返回还是继续循环?(如果我们继续,如果它是最后一个要迭代的项目呢?那么我们就会在foreach之外继续,而返回永远不会发生?)编辑:甚至可以使用goto到循环外的标签,几乎任何将控制转移出foreach循环的操作。 - Chris Sinclair
2
我不同意"When you literally have catch(Exception) then you don't need the finally (and probably not even the continue)."。如果我需要在异常发生与否都执行某个操作怎么办? - lpaloub
好的,finally 处理块内部的额外 return。但通常在 catch-all 之后继续执行。 - H H
“finally”不是“catch”。它应该用于清理代码。无论是否抛出异常,finally块都会运行。像关闭文件或释放内存(如果您正在使用非托管代码)等内容,这些都应该放在finally块中。 - Robotnik
轮到我了。@Henk - 这些靴子是为了行走而生的。我提出 finally 部分,因为你说如果有 catch-all 就不需要它。 - Robotnik

3

finally块中不能离开其所属的函数体,包括break、return和在您的情况下continue关键字。


3
finally语句块可能会在等待重新抛出的异常情况下执行。如果没有重新抛出异常,那么退出该语句块(通过continue或其他方式)就没有意义。
如果您希望无论发生什么都继续循环,那么您不需要finally语句:只需捕获异常并不重新抛出即可。

1
finally无论是否抛出未捕获的异常都会执行。其他人已经解释了为什么这使得continue不合逻辑,但是这里有一个替代方案,它遵循了这段代码所要求的精神。基本上,finally { continue; }的意思是:
  1. 当有捕获的异常时,继续执行
  2. 当有未捕获的异常时,允许它们被抛出,但仍然继续执行
(1)可以通过在每个catch的末尾放置continue来满足,而(2)可以通过存储未捕获的异常以便稍后抛出来满足。你可以像这样编写它:
var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

实际上,在第三种情况下,即没有任何异常被抛出、捕获或未捕获的情况下,finally也会执行。如果需要这样做,当然可以在try-catch块后面放置一个单独的continue,而不是在每个catch中放置。

1
从技术上讲,这是基础CIL的限制。来自语言规范

除非通过异常处理机制,否则不允许控制转移进入catch处理程序或finally子句。

保护区域外的控制转移仅允许通过异常指令(leaveend.filterend.catchend.finally)进行

br指令的文档页面上:

此指令无法执行进入和退出try、catch、filter和finally块的控制转移。

这个最后一个适用于所有分支指令,包括beqbrfalse等。

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