我正在实现我自己的可枚举类型。重新安排的事情:
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()应该创建一个新的,还是应该总是返回相同的实例?
答案 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
方法本身,您应该在尝试搜索之前检查流是否实际可搜索 - 许多流不是(例如网络流)。