为什么收益率回报不能出现在带有捕获的try块内?


95

没关系:

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块有什么问题?

当然,收益率返回部分(当前)无法抛出任何东西,但是为什么那应该阻止我们封闭try/ catch处理在a之前或之后抛出的异常yield return呢?

更新:这里有来自Eric Lippert有趣评论 -似乎他们已经有足够的问题才能正确实现try / finally行为!

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


2
指向埃里克·利珀特(Eric Lippert)评论的直接链接:blogs.msdn.com/oldnewthing/archive/2008/08/14/…–
罗曼·斯塔科夫

注意:您也不能在catch块本身中屈服:-(
Simon_Weaver

2
oldnewthing链接不再起作用。
塞巴斯蒂安·雷德尔

Answers:


50

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

我已经遇到过一些类似的事情:

  • 属性不能通用
  • X无法从XY派生(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块-但这确实是编译器可以做的。

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


11
一个很好的概念证明,但是一旦您开始使用using和来创建作用域,这种策略就变得很痛苦(对于C#程序员而言,可能比对C#编译器编写者而言要痛苦得多)foreach。例如:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian

“ try / catch”的正常语义意味着,如果由于异常而跳过了try / catch块的任何部分,则控制将转移到合适的“ catch”块(如果存在)。不幸的是,如果在“收益率回报”期间发生异常,则迭代器无法区分由于异常而将其处置的情况与由于所有者检索了所有感兴趣的数据而将其处置的情况。
supercat

7
“我怀疑很少有这种限制实际上是无法解决的问题”,这就像在说您不需要例外,因为您可以使用C语言中常用的错误代码返回策略,因此多年前。我承认技术上的困难可能很大,但是yield在我看来,这仍然严重限制了实用性,因为您必须编写意大利细面条代码才能解决该问题。
jpmc26 2013年

@ jpmc26:不,这根本不像是在说那样。我不记得曾经有过这样经历,而且我已经使用迭代器块很多次了。它稍微限制了实用性yieldIMO -这是从一个很长的路要走严重
乔恩·斯基特

2
这个“功能”实际上需要在某些情况下,要解决一些非常丑陋的代码,看到stackoverflow.com/questions/5067188/...
namey

5

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


2
我不认为我买那个。我认为这将是完全可行的-但非常复杂。
乔恩·斯基特

2
C#中的try / catch块并不意味着可以重入。如果将它们拆分开,则可以在出现异常后调用MoveNext()并以可能无效的状态继续try块。
Mark Cidade

2

我推测,由于当您从枚举器中获得收益时,调用堆栈会被缠绕/解绕,因此try / catch块实际上无法“捕获”异常。(因为yield return块不在堆栈中,即使他是迭代块的发起者)

要获得我正在谈论的想法,请设置迭代器块和使用该迭代器的foreach。检查在foreach块内的调用堆栈是什么样子,然后在迭代器try / finally块内检查它。


我熟悉C ++中的堆栈展开,其中在超出范围的本地对象上调用析构函数。C#中的相应内容将是try / finally。但是,当收益率发生时,这种放松就不会发生。对于try / catch,不需要它与收益回报进行交互。
Daniel Earwicker

检查循环遍历迭代器时调用堆栈发生了什么,您将了解我的意思
Radu094

@ Radu094:不,我相信这是可能的。不要忘记它已经可以处理了,至少有些相似。
乔恩·斯基特

2

我接受了INVINCIBLE SKEET的回答,直到Microsoft有人来为这个主意倒水。但是我不同意问题部分-当然,正确的编译器比完整的编译器更为重要,但是C#编译器已经非常聪明地为我们整理了这种转换。在这种情况下,完整性的提高将使语言更易于使用,教授,解释,同时减少边缘情况或麻烦。因此,我认为值得付出额外的努力。雷德蒙德(Redmond)的几个人在两周内挠头,结果,在接下来的十年中,数百万的编码人员可以再放松一点。

(我也怀着一种强烈的愿望,希望有一种方法可以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");

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


1
是的,您可以将评估结果放入try / catch。放置变量设置的位置并不重要。要点是,您可以有效地将一个单次尝试/捕获(其中包含收益收益)分解为两个尝试/捕获(它们之间具有收益收益)。
乔恩·斯基特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.