问题 为什么IEumerator 会影响IEnumerable 的状态,即使枚举器从未到达终点?


我很好奇为什么以下内容在“最后”赋值时抛出错误消息(文本阅读器关闭异常):

IEnumerable<string> textRows = File.ReadLines(sourceTextFileName);
IEnumerator<string> textEnumerator = textRows.GetEnumerator();

string first = textRows.First();
string last = textRows.Last();

但是以下执行正常:

IEnumerable<string> textRows = File.ReadLines(sourceTextFileName);

string first = textRows.First();
string last = textRows.Last();

IEnumerator<string> textEnumerator = textRows.GetEnumerator();

不同行为的原因是什么?


3190
2017-12-26 10:53


起源

实际上,这两个代码都在我的机器上崩溃...... - digEmAll
@digEmAll,第二个对我来说很好,当我尝试确定文本文件中的最后一行时,第一个代码中断。 - Matt
@digEmAll:这很奇怪 - 第二个代码对我来说很好,我理解 为什么 它工作正常。你看到了什么问题,在哪里? - Jon Skeet
@JonSkeet,第二个代码对我来说也失败了,同样的错误并在同一行。 - Andrei
@Andrei:嗯。您使用的是哪个版本的.NET?这是在调试器中吗? - Jon Skeet


答案:


据我所知,你在框架中发现了一个错误。由于一些事物的相互作用,这是相当微妙的:

  • 你打电话时 ReadLines(),文件实际上是打开的。就个人而言,我认为这本身就是一个错误;我希望并希望它是懒惰的 - 只有当你尝试开始迭代它时才打开文件。
  • 你打电话时 GetEnumerator() 该 第一 时间在返回值上 ReadLines,它实际上会返回相同的引用。
  • 什么时候 First() 电话 GetEnumerator(),它将创建一个克隆。这将分享相同的 StreamReader 如 textEnumerator
  • 什么时候 First() 处理它的克隆,它将处理它 StreamReader,并设置 它的 变量到 null。这不会影响原始变量,现在指的是已处置的变量 StreamReader
  • 什么时候 Last() 电话 GetEnumerator(),它将创建一个克隆 原始对象,配有配置 StreamReader。然后它尝试从该读取器读取,并抛出异常。

现在将其与第二个版本进行比较:

  • 什么时候 First() 电话 GetEnumerator(),返回原始参考,完成开放阅读器。
  • 什么时候 First() 然后打电话 Dispose(),读者将被处理并且变量设置为 null
  • 什么时候 Last() 电话 GetEnumerator(),将创建一个克隆 - 但因为它克隆的值有一个 null 参考,一个新的 StreamReader 已创建,因此它可以毫无问题地读取文件。然后它处理克隆,关闭阅读器
  • 什么时候 GetEnumerator() 被称为原始对象的第二个克隆,打开另一个 StreamReader  - 再次,那里没有问题。

所以基本上,第一个片段中的问题是你正在打电话 GetEnumerator() 第二次(在 First())没有处理第一个物体。

这是同一问题的另一个例子:

using System;
using System.IO;
using System.Linq;

class Test
{
    static void Main()
    {
        var lines = File.ReadLines("test.txt");
        var query = from x in lines
                    from y in lines
                    select x + "/" + y;
        foreach (var line in query)
        {
            Console.WriteLine(line);
        }
    }
}

你可以通过调用解决这个问题 File.ReadLines 两次 - 或者通过使用真正懒惰的实现 ReadLines, 喜欢这个:

using System.IO;
using System.Linq;

class Test
{
    static void Main()
    {
        var lines = ReadLines("test.txt");
        var query = from x in lines
                    from y in lines
                    select x + "/" + y;
        foreach (var line in query)
        {
            Console.WriteLine(line);
        }
    }

    static IEnumerable<string> ReadLines(string file)
    {
        using (var reader = File.OpenText(file))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }
}

在后面的代码中,一个新的 StreamReader 每次都打开 GetEnumerator() 被调用 - 所以结果是test.txt中的每对行。


12
2017-12-26 11:11



谢谢你的详细解释。非常有趣,因为我不知道First()或Last()处理内部迭代器/枚举器,所以我不得不深入挖掘。 - Matt
@Freddy: 一切 它使用了 IEnumerator<T> 应该处理它。 - Jon Skeet
我明白你的意思但在这种情况下调用First()和Last()我希望它不会处理底层的文本阅读器。在那我仍然有点困惑,一方面必须手动处理,另一方面,对象在调用First()或Last()后自行处理... - Matt
@JonSkeet:我刚看了.net 4.5源代码。他们使用IEnumerator实现改变了基于yield的代码,试图保持相同的行为。无论如何,我已经在.net 4.0项目中复制了代码而第二种情况正常工作,同时使用.net 4 File.ReadLines它没有...... - digEmAll
@Freddy:不,打电话 First() 要么 Last() 会创造(通过 GetEnumerator()),然后阅读,然后处置。 - Jon Skeet