【问题标题】:Why do iterators behave differently on exception than LINQ enumerables?为什么迭代器的异常行为与 LINQ 可枚举不同?
【发布时间】:2020-02-04 00:14:36
【问题描述】:

我正在研究iterator 方法的内部机制,我注意到迭代器获得的IEnumerator<T> 和LINQ 方法获得的IEnumerator<T> 之间的行为存在奇怪的差异。如果枚举过程中发生异常,那么:

  1. LINQ 枚举器保持活动状态。它会跳过一个项目,但会继续生产更多项目。
  2. 迭代器枚举器已完成。它不再生产任何物品。

示例。一个IEnumerator<int> 被顽固地枚举,直到它完成:

private static void StubbornEnumeration(IEnumerator<int> enumerator)
{
    using (enumerator)
    {
        while (true)
        {
            try
            {
                while (enumerator.MoveNext())
                {
                    Console.WriteLine(enumerator.Current);
                }
                Console.WriteLine("Finished");
                return;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception: {ex.Message}");
            }
        }
    }
}

让我们尝试枚举一个 LINQ 枚举器,它会在每 3 个项目上抛出一次:

var linqEnumerable = Enumerable.Range(1, 10).Select(i =>
{
    if (i % 3 == 0) throw new Exception("Oops!");
    return i;
});
StubbornEnumeration(linqEnumerable.GetEnumerator());

输出:

1
2
例外:糟糕!
4
5
例外:糟糕!
7
8
例外:糟糕!
10
完成了

现在让我们用一个迭代器尝试同样的方法,它会在每 3 个项目上抛出一个:

StubbornEnumeration(MyIterator().GetEnumerator());

static IEnumerable<int> MyIterator()
{
    for (int i = 1; i <= 10; i++)
    {
        if (i % 3 == 0) throw new Exception("Oops!");
        yield return i;
    }
}

输出:

1
2
例外:糟糕!
完成了

我的问题是:这种不一致的原因是什么?哪种行为对实际应用更有用?

注意:此观察是在另一个与迭代器相关的问题中的 answer by Dennis1679 之后进行的。


更新:我做了更多观察。并非所有 LINQ 方法的行为都相同。例如,Take 方法在内部实现为 .NET Framework 上的 TakeIterator,因此它的行为类似于迭代器(异常立即完成)。但在 .NET Core 上,它的实现方式可能有所不同,因为在异常情况下它会继续运行。

【问题讨论】:

  • 第一个代码示例应该很清楚了。由于您正在捕获每个异常,因此外部循环不会中断。 yield return 的示例可能看起来有点奇怪,但编译器在幕后做了很多工作以使其像那样工作。
  • @Dennis_E 相同的方法StubbornEnumeration 用于枚举两个枚举器,即LINQ 和迭代器。结果是不同的。老实说,我没想到会存在这种差异。

标签: c# linq iterator enumeration


【解决方案1】:

第二个示例中的yield 语法有所不同。当您使用它时,编译器会生成一个状态机,该状态机在后台管理一个真正的枚举器。抛出异常会退出函数并因此终止状态机。

【讨论】:

  • 有没有办法重写我的迭代器,使其表现得像 LINQ 枚举?
  • 是的,您可以构建自己的 IEnumerableIEnumerator 实现(这正是 LINQ 在幕后所做的),允许异常从 MoveNext() 冒出而无需设置其内部状态完成。
  • 您有什么解释为什么迭代器的实现方式与日常开发人员手动编写IEnumerable/IEnumerator 对的方式不一致吗?
  • 根据上下文,两者都是合理的。在 LINQ 案例中,我们从 lambda 内部抛出异常,该异常在每次迭代时显式运行一次。在yield 的情况下,如果我们抛出异常但以某种方式继续运行函数而不是离开它,那么这里会有很多问题试图理解和解释这种行为!
  • 我做了这个实验。我将迭代器复制粘贴到SharpLab.io,获取生成的代码,修复非法变量名以使其编译,并将其用作StubbornEnumeration 的参数。它的行为类似于迭代器。
猜你喜欢
  • 2010-09-06
  • 2020-05-17
  • 1970-01-01
  • 2011-03-13
  • 1970-01-01
  • 2012-06-08
  • 1970-01-01
  • 2016-09-07
  • 2010-11-06
相关资源
最近更新 更多