【问题标题】:Recommended behaviour of GetEnumerator() when implementing IEnumerable<T> and IEnumerator<T>实现 IEnumerable<T> 和 IEnumerator<T> 时 GetEnumerator() 的推荐行为
【发布时间】:2011-10-06 10:39:47
【问题描述】:

我正在实现自己的可枚举类型。类似这样的东西:

public class LineReaderEnumerable : IEnumerable<string>, IDisposable
{
    private readonly LineEnumerator enumerator;

    public LineReaderEnumerable(FileStream fileStream)
    {
        enumerator = new LineEnumerator(new StreamReader(fileStream, Encoding.Default));
    }

    public IEnumerator<string> GetEnumerator()
    {
        return enumerator;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Dispose()
    {
       enumerator.Dispose();
    }
}

枚举器类:

public class LineEnumerator : IEnumerator<string>
{
    private readonly StreamReader reader;
    private string current;

    public LineEnumerator(StreamReader reader)
    {
        this.reader = reader;
    }

    public void Dispose()
    {
        reader.Dispose();
    }

    public bool MoveNext()
    {
        if (reader.EndOfStream)
        {
            return false;
        }
        current = reader.ReadLine();
        return true;
    }

    public void Reset()
    {
        reader.DiscardBufferedData();
        reader.BaseStream.Seek(0, SeekOrigin.Begin);
        reader.BaseStream.Position = 0;
    }

    public string Current
    {
        get { return current; }
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }
}

我的问题是:我应该在调用 GetEnumerator() 时在枚举器上调用 Reset() 还是调用方法(如 foreach)的责任?

GetEnumerator() 应该创建一个新实例,还是应该始终返回相同的实例?

【问题讨论】:

标签: c# .net ienumerable


【解决方案1】:

您的模型从根本上被破坏了 - 您应该在每次调用 GetEnumerator() 时创建一个新的 IEnumerator&lt;T&gt;。迭代器是相互独立的。例如,我应该会写:

var lines = new LinesEnumerable(...);
foreach (var line1 in lines)
{
    foreach (var line2 in lines)
    {
        ...
    }
}

并且基本上得到文件中每一行与其他每一行的叉积。

这意味着LineEnumerable 类应该被赋予FileStream - 它应该被赋予一些可以用来获得FileStream 的东西每次你需要一个,例如一个文件名。

例如,您可以使用迭代器块在单个方法调用中完成所有这些操作:

// Like File.ReadLines in .NET 4 - except that's broken (see comments)
public IEnumerable<string> ReadLines(string filename)
{
    using (TextReader reader = File.OpenText(filename))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

然后:

var lines = ReadLines(filename);
// foreach loops as before

...这样就可以了。

编辑:请注意,某些序列自然只能迭代一次 - 例如网络流,或来自未知种子的随机数序列。

这样的序列确实更好地表示为IEnumerator&lt;T&gt; 而不是IEnumerable&lt;T&gt;,但这使得使用LINQ 进行过滤等变得更加困难。 IMO 此类序列应该至少在第二次调用 GetEnumerator() 时抛出异常 - 返回相同的迭代器两次是一个非常糟糕的主意。

【讨论】:

  • 这是在哪里记录的?请引用?
  • @Noldorin 的例子不是很明显吗?
  • @Noldorin:我想说,IEnumerator 的文档中隐含了它。
  • 如果我没记错的话,File.ReadLines 返回的 Enumerable 不能被迭代两次。
  • @CodeInChaos: Ick - 看起来你不仅是对的,而且它实际上返回了相同的迭代器两次。所以我的 ReadLines 方法更好:) 序列只能迭代一次是可以的,但是 GetEnumerator() 应该在第二次调用时失败,而不是返回第一个迭代器。将编辑。
【解决方案2】:

您这种类型的用户的期望是GetEnumerator() 返回一个新的枚举器对象。

正如你所定义的,每次调用GetEnumerator都会返回相同的枚举器,所以代码如下:

var e1 = instance.GetEnumerator();
e1.MoveNext();
var first = e1.Value();

var e2 = instance.GetEnumerator();
e2.MoveNext();
var firstAgain = e2.Value();

Debug.Assert(first == firstAgain);

不会按预期工作。

(对Reset 的内部调用将是一个不寻常的设计,但这是次要的。)

附加: PS如果您想在文件的行上使用枚举器,请使用File.ReadLines,但它会出现(请参阅Jon Skeet's 上的 cmets 答案)这个遇到与您的代码相同的问题。

【讨论】:

  • 好的,我每次都会返回一个新的枚举数。这实际上是个问题,因为我从不直接使用 GetEnumerator,所以我想知道是否必须创建一个新的。我会从文件名创建它,但我绑定到我无法更改的库接口。
【解决方案3】:

GetEnumerator() 应该创建一个新的,还是应该总是 返回相同的实例?

如果您返回相同的实例,则第二次迭代将从第一次迭代的位置返回结果,如果代码交替或并行执行,它们将相互干扰。所以不,你不应该返回相同的实例。

重置

只要集合保持不变,枚举数就保持有效。如果进行了更改 到集合,例如添加、修改或删除元素, 枚举器不可恢复地失效,下一次调用 MoveNext 或 Reset 方法会引发 InvalidOperationException。

为 COM 互操作性提供了 Reset 方法。它不是 必然需要实施;相反,实施者可以 只需抛出 NotSupportedException。

http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx

【讨论】:

  • 事实上,Reset 几乎从未实现过。
【解决方案4】:

我的问题是:我应该在调用 GetEnumerator() 时在枚举器上调用 Reset() 还是调用方法(如 foreach)的责任?

那是调用方法的职责;但是,如果您的枚举器在第一次调用 Reset() 之前无效,那么您当然应该在返回它之前调用它(这将是一个实现细节)。

在正常操作中,枚举器永远不会真正重置。您可以通过在 reset 中抛出 NotSupportedException 来验证这一点。

GetEnumerator() 应该创建一个新实例,还是应该始终返回相同的实例?

是的,它应该总是返回一个新实例。可以这样想:Enumerable 是您可以枚举的东西。 Enumerator 是您用来枚举的事物。如果 GetEnumerator() 总是返回相同的实例,则包含的类将不是“可枚举”,而只是知道如何“枚举”(IOW:它只是 IHasEnumerator 而不是 IEnumerable

【讨论】:

    【解决方案5】:

    就我而言,这应该是调用者的责任。这来自 POLA(principle of least astonishment,如果你愿意的话。事实上,你不希望你的读者控制太多。考虑一下,如果消费者只想从流中的某个点开始枚举行怎么办?

    关于Reset 方法本身,您应该在尝试搜索之前真正检查流是否真的可搜索——许多流不是(例如网络流)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-09
      • 1970-01-01
      • 2021-08-25
      • 1970-01-01
      • 1970-01-01
      • 2014-11-17
      • 2011-07-29
      • 1970-01-01
      相关资源
      最近更新 更多