实现IEnumerable <t>和IEnumerator <t> </t> </t>时GetEnumerator()的推荐行为

时间:2011-10-06 10:39:47

标签: c# .net ienumerable

我正在实现我自己的可枚举类型。重新安排的事情:

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()应该创建一个新的,还是应该总是返回相同的实例?

5 个答案:

答案 0 :(得分:7)

您的模型从根本上被打破 - 每次调用IEnumerator<T>时都应创建一个新的GetEnumerator()。迭代器意味着彼此独立。例如,我应该能够写:

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

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

这意味着LineEnumerable应该被赋予FileStream - 它应该被赋予一些可用于获取 a {{ 1}}每次你需要一个,例如文件名。

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

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<T>,但这使得LINQ的过滤等更难。 IMO这样的序列应该至少在第二次调用IEnumerable<T>时抛出异常 - 两次返回相同的迭代器是一个非常糟糕的主意。

答案 1 :(得分:5)

您的类型用户的期望是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,但它会显示(请参阅{{3}上的评论}答案)这会遇到与您的代码相同的问题。

答案 2 :(得分:2)

  

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

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

重置

  

只要集合保持不变,枚举器仍然有效。如果进行了更改   到集合,例如添加,修改或删除元素,   调查员无法恢复无效,下次调用   MoveNext或Reset方法抛出InvalidOperationException。

     

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

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

答案 3 :(得分:0)

  

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

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

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

  

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

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

答案 4 :(得分:-1)

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

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