实现 IEnumerable时 GetEnumerator() 的推荐行为和 IEnumerator

发布于 2024-12-08 14:17:30 字数 1538 浏览 0 评论 0原文

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

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

I am implementing my own enumerable type. Something ressembling this:

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();
    }
}

The enumerator class:

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; }
    }
}

My question is this: should I call Reset() on the enumerator when GetEnumerator() is called or is it the responsability of the calling method (like foreach) to do it?

Should GetEnumerator() create a new one, or is it supposed to always return the same instance?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

自找没趣 2024-12-15 14:17:30

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

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;
        }
    }
}

Then:

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

...这将很好地工作。

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

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

Your model is fundamentally broken - you should create a new IEnumerator<T> each time GetEnumerator() is called. Iterators are meant to be independent of each other. For example, I ought to be able to write:

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

and basically get the cross-product of each line in the file against each of the other lines.

This means LineEnumerable class should not be given a FileStream - it should be given something which can be used to obtain a FileStream each time you need one, e.g. a filename.

For example, you can do all of this in a single method call using iterator blocks:

// 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;
        }
    }
}

Then:

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

... that will work fine.

EDIT: Note that certain sequences are naturally iterable only once - e.g. a network stream, or a sequence of random numbers from an unknown seed.

Such sequences are really better expressed as IEnumerator<T> rather than IEnumerable<T>, but that makes filtering etc with LINQ harder. IMO such sequences should at least throw an exception on the second call to GetEnumerator() - returning the same iterator twice is a really bad idea.

汹涌人海 2024-12-15 14:17:30

您类型的用户的期望是 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的回答)这也遇到了同样的问题作为你的代码。

The expectation of a user of your type would be that GetEnumerator() returns a new enumerator object.

As you have defined it every call to GetEnumerator returns the same enumerator, so code like:

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

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

Debug.Assert(first == firstAgain);

will not work as expected.

(An internal call to Reset would be an unusual design, but that's secondary here.)

Additional: PS If you want an enumerator over the lines of a file then use File.ReadLines, but it appears (see comments on Jon Skeet's answer) this suffers from the same problem as your code.

左岸枫 2024-12-15 14:17:30

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

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

用于重置

只要集合保持不变,枚举器就保持有效。如果做出改变
到集合中,例如添加、修改或删除元素,
枚举器将不可恢复地失效,并且下次调用
MoveNext 或 Reset 方法抛出 InvalidOperationException。

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

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

Should GetEnumerator() create a new one, or is it supposed to always
return the same instance?

If you return the same instance then the second iteration will be returning results from the point where the first iteration is and both of them will interfere with each other if the code is executing alternatively or in parallel. so No you shouldn't return same instance.

For Reset

An enumerator remains valid as long as the collection remains unchanged. If changes are made
to the collection, such as adding, modifying, or deleting elements,
the enumerator is irrecoverably invalidated and the next call to the
MoveNext or Reset method throws an InvalidOperationException.

The Reset method is provided for COM interoperability. It does not
necessarily need to be implemented; instead, the implementer can
simply throw a NotSupportedException.

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

尾戒 2024-12-15 14:17:30

我的问题是:当调用 GetEnumerator() 时,我应该在枚举器上调用 Reset() 还是由调用方法(如 foreach)负责执行此操作?

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

在正常操作中,枚举器实际上不会被重置。您可以通过从重置中抛出 NotSupportedException 来验证这一点。

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

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

My question is this: should I call Reset() on the enumerator when GetEnumerator() is called or is it the responsability of the calling method (like foreach) to do it?

That is the responsability of the calling method; However if your enumerator is invalid before a first call to Reset() you should of course call it before returning it (that would be an implementation detail).

In normal operation, an enumerator is never actually reset. You can verify that by throwing NotSupportedException from within reset.

Should GetEnumerator() create a new one, or is it supposed to always return the same instance?

Yes it should always return a new instance. Think of it this way: an Enumerable is something that you can enumerate. Enumerator is the thing that you use to enumerate with. If GetEnumerator() always returned the same instance, the containing class would not be 'enumerable' but just know how to 'enumerate' (IOW: it would just be IHasEnumerator instead of IEnumerable)

执着的年纪 2024-12-15 14:17:30

就我而言,这应该是打电话者的责任。这遵循 POLA(最小惊讶原则,如果你愿意的话。事实上,你不 ,如果消费者只想枚举从流中的某个点开始的行怎么办?

不希望你的读者控制太多。考虑一下 在尝试查找之前,您应该真正检查一下该流是否实际上是可查找的——许多流不是(例如网络流)。

As far as I'm concerned, it should be the responsibility of the caller. This follows from POLA (the principle of least astonishment, if you like. And indeed, you don't want your reader to control too much. Consider, what if the consumer wants only to enumerate lines from a certain point in the stream onwards?

And regarding the Reset method itself, you should really check to see if the stream is actually seekable before trying to seek -- many streams aren't (e.g. network streams).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文