109

以下是可以的:

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);
}

假设(为了论证)WriteLinetry 块内的一个或其他调用引发了异常。继续执行catch块有什么问题?

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

更新:这里有来自 Eric Lippert的有趣评论- 似乎他们在正确实施 try/finally 行为时已经遇到了足够多的问题!

编辑:有关此错误的 MSDN 页面是:http: //msdn.microsoft.com/en-us/library/cs1x15az.aspx。但是,它没有解释原因。

4

4 回答 4

54

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

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

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

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

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

考虑一下:

  • 您可以确保 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 块 - 但这是编译器当然可以做的事情。

我很可能在这里错过了一些东西 - 如果是这样,请告诉我!

于 2008-12-06T21:07:02.020 回答
6

迭代器定义中的所有yield语句都转换为状态机中的状态,该状态机有效地使用switch语句来推进状态。如果它确实yield为try/catch 中的语句生成代码,则必须为每个语句复制块中的所有内容,同时排除该块的所有其他语句。这并不总是可能的,特别是如果一个语句依赖于较早的语句。try yieldyieldyield

于 2008-12-06T16:15:59.360 回答
3

我已经接受了 THE INVINCIBLE SKEET 的回答,直到微软的某个人来给这个想法泼冷水。但我不同意意见问题——当然,正确的编译器比完整的编译器更重要,但 C# 编译器已经非常聪明地为我们整理出这种转换。在这种情况下,更完整一点将使语言更易于使用、教学、解释,并减少边缘情况或陷阱。所以我认为付出额外的努力是值得的。雷德蒙德的一些人会在两周内摸不着头脑,因此在接下来的十年中,数以百万计的程序员可以放松一点。

(我还怀有一种肮脏的愿望,希望有一种方法可以yield return通过驱动迭代的代码“从外部”填充到状态机中的异常抛出。但我想要这个的原因非常模糊。)

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

显然 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");

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

于 2008-12-07T00:38:43.917 回答
2

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

为了了解我正在谈论的内容,使用该迭代器设置一个迭代器块和一个 foreach。检查调用堆栈在 foreach 块中的样子,然后在迭代器 try/finally 块中检查它。

于 2008-12-06T15:43:20.643 回答