为什么不能在带有catch的try块内出现yield return?

时间:2008-12-06 15:14:36

标签: c# exception yield-keyword

以下是可以的:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

finally块在整个事件执行完毕后运行(IEnumerator<T>支持IDisposable,以便在枚举完成之前放弃枚举时提供确保这一点的方法。

但这不合适:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

假设(为了参数)try块中的一个或另一个WriteLine调用抛出异常。在catch块中继续执行有什么问题?

当然,yield return部分(目前)无法抛出任何东西,但是为什么要阻止我们使用封闭的try / catch来处理在{之前或之后抛出的异常{1}}?

更新:interesting comment from Eric Lippert here - 似乎他们已经在正确实施try / finally行为方面遇到了足够的问题!

编辑:此错误的MSDN页面是:http://msdn.microsoft.com/en-us/library/cs1x15az.aspx。但这并没有解释为什么。

5 个答案:

答案 0 :(得分:48)

我怀疑这是一个实用性而非可行性的问题。我怀疑很少有这种限制实际上是一个无法解决的问题 - 但编译器中增加的复杂性将是非常重要的。

我已经遇到过这样的一些事情:

  • 属性不能通用
  • X无法从X.Y(X中的嵌套类)派生
  • 在生成的类中使用公共字段的迭代器块

在每种情况下,都可以获得更多的自由,但代价是编译器的额外复杂性。团队做出了务实的选择,我为此鼓掌 - 我宁愿用一种稍微限制一点的语言,准确率达到99.9%的编译器(是的,有错误;我在前几天碰到了一个)比一个更多灵活的语言,无法正确编译。

编辑:这是一个伪证据,说明它为何可行。

考虑一下:

  • 您可以确保yield return部分本身不会抛出异常(预先计算该值,然后您只是设置一个字段并返回“true”)
  • 你可以在迭代器块中使用不使用yield return的try / catch。
  • 迭代器块中的所有局部变量都是生成类型中的实例变量,因此您可以自由地将代码移动到新方法

现在转换:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

进入(伪代码):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

唯一的重复是设置try / catch块 - 但这是编译器当然可以做的事情。

我可能在这里遗漏了一些东西 - 如果有的话,请告诉我!

答案 1 :(得分:5)

迭代器定义中的所有yield语句都转换为状态机中的状态,该状态机有效地使用switch语句来提升状态。如果 在try / catch中生成yield语句的代码,则必须在try块中复制所有 yield语句,同时排除该块的每个其他yield语句。这并不总是可行的,特别是如果一个yield语句依赖于前一个语句。

答案 2 :(得分:2)

我推测,由于调用栈在从枚举器返回时被缠绕/解除的方式,try / catch块实际上不可能“捕获”异常。 (因为yield返回块不在堆栈中,即使他发起了迭代块)

要了解我正在讨论的内容,请使用该迭代器设置迭代器块和foreach。检查foreach块中调用堆栈的内容,然后在迭代器try / finally块中检查它。

答案 3 :(得分:2)

我已经接受了无法回答的问题,直到微软的某个人为这个想法倾注了冷水。但我不同意意见问题部分 - 当然正确的编译器比完整编译器更重要,但C#编译器已经非常聪明地为我们整理了这个转换。在这种情况下,更完整一点将使语言更容易使用,教学,解释,边缘情况或陷阱更少。所以我觉得值得付出额外的努力。雷德蒙德的一些人在他们的头上划了两个星期,结果在接下来的十年中,数百万的程序员可以放松一下。

(我还希望有一种方法让yield return通过驱动迭代的代码抛出一个“从外部”填充到状态机中的异常。但我的理由因为想要这个是非常模糊的。)

实际上,我对Jon的回答有一个问题是关于yield return表达式抛出。

显然收益率10并不是那么糟糕。但这很糟糕:

yield return File.ReadAllText("c:\\missing.txt").Length;

因此,在前面的try / catch块中评估它是否更有意义:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

下一个问题是嵌套的try / catch块和重新抛出的异常:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

但我确信这是可能的......

答案 4 :(得分:-6)

对于那些使用Unity的人:

yield return new WaitForSeconds(startWait);
while (numWaves < 4 && _myPauseState)
{
for (int i = 0; i < hazardCount;)
{
//spawn code
}
yield return new WaitForSeconds(waveWait);
numWaves++;
}

实际上可以在ienumerator中使用