问题 .net framework 4.0的File.ReadLines(..)方法中的错误


这段代码:

IEnumerable<string> lines = File.ReadLines("file path");
foreach (var line in lines)
{
    Console.WriteLine(line); 
}
foreach (var line in lines)
{ 
    Console.WriteLine(line); 
} 

抛出一个 ObjectDisposedException : {"Cannot read from a closed TextReader."} 如果第二个 foreach 被执行。 似乎从中返回了迭代器对象 File.ReadLines(..) 不能一次列举。您必须通过调用获取新的迭代器对象 File.ReadLines(..) 然后用它来迭代。

如果我更换 File.ReadLines(..) 与我的版本(参数未经验证,这只是一个例子):

public static IEnumerable<string> MyReadLines(string path)
{
    using (var stream = new TextReader(path))
    {
        string line;
        while ((line = stream.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

可以多次迭代文件的行。

使用调查 .Net Reflector 表明执行了 File.ReadLines(..) 叫私人 File.InternalReadLines(TextReader reader) 创建实际的迭代器。作为参数传递的阅读器用于 MoveNext() 迭代器的方法来获取文件的行,并在我们到达文件末尾时处理。这意味着一次 MoveNext() 返回false没有办法再次迭代,因为读取器已关闭,你必须通过创建一个新的迭代器来获得一个新的读取器 ReadLines(..) method.In我的版本中创建了一个新的阅读器 MoveNext() 每次我们开始一个新的迭代时的方法。

这是预期的行为吗? File.ReadLines(..) 方法?

我发现在你枚举结果之前每次调用方法都很麻烦。每次迭代使用该方法的Linq查询的结果之前,您还必须调用该方法。


4904
2018-02-20 23:57


起源

“这是File.ReadLines(..)方法的预期行为吗?”是。如果你已经消费了 StreamReader 它将被处置。没有办法来回。如果你需要,你必须使用 File.ReadAllLines。 - Tim Schmelter
实际上,一个简单的解决方法就像 IEnumerable<string> ReadLinesFixed(string path) { foreach (var line in File.ReadLines(path)) yield return line; } 也有效。 - Vlad


答案:


我知道这是旧的,但实际上我在Windows 7机器上处理一些代码时遇到了这个问题。与人们在这里所说的相反,这实际上就是这样  一个bug。看到 这个链接

因此,简单的解决方法是更新您的.net framefork。我认为这值得更新,因为这是最热门的搜索结果。


6
2017-12-21 15:42





我不认为这是一个错误,我不认为这是不寻常的 - 事实上,这是我期望的文本文件阅读器之类的东西。 IO是一项昂贵的操作,因此通常您希望一次性完成所有操作。


5
2018-02-21 00:01



是的,但读者可以在IEnumerable.GetEnumerator调用中创建,即枚举开始时,而不是创建IEnumerable时。我同意Adrian,这将是更可预测的行为,并且更容易使用新方法旨在支持的LINQ运算符(并且由于它们是懒惰的,因此与那些LINQ运算符更加一致)。 - itowlson


这不是一个bug。但我相信你可以使用ReadAllLines()来做你想做的事情。 ReadAllLines创建一个字符串数组并将所有行拉入数组,而不是像ReadLines那样只是一个简单的枚举器。


1
2018-02-21 00:11



正如我之前提到的那样,在我可以使用数组中的数据之前,我宁愿不等待返回整个数组。通常情况下,当文件很大并且最终在内存中有一个100 MB的数组时。我可以在返回整个集合之前开始枚举行。 - Adrian Constantin
我很少见到有人在努力争取一个好问题的好答案。显然这不是一个错误。文档解释了行为,解释与实际行为相符。有两种方法,一种允许在只读流上进行简单的非缓冲枚举。对于需要可重用缓冲区的情况,另一个将内容缓冲到数组。返回类型符合此意图。无缓冲的返回IEnumerable。缓冲的一个返回一个数组。仅这一点就使得两种不同方法的意图非常明确。 - Stephen M. Redd
使用数组,您无法在数组完全加载之前启动枚举。在迭代时,数组会发生变化,这是明确禁止的。您似乎建议您想要一个稍后可以像数组一样处理的流。没关系。有类似的对象,特别是在各种LINQ实现中。但这不是什么 这些 特殊方法。像任何东西一样,你可以使用这些和类似的方法来做你想要的更复杂的事情。只要写一个以这种方式做事的课程。 - Stephen M. Redd


如果你需要两次访问这些行,你可以随时将它们缓冲到一个 List<T>

using System.Linq;

List<string> lines = File.ReadLines("file path").ToList(); 
foreach (var line in lines) 
{ 
    Console.WriteLine(line);  
} 
foreach (var line in lines) 
{  
    Console.WriteLine(line);  
} 

0
2018-02-21 00:09



麻烦的是,这需要.NET来阅读全部内容 立刻,对于大文件来说效率可能非常低。 ReadLines方法的重点是避免使用它(正如Stephen指出的那样,已经由ReadAllLines充分处理)。 - itowlson
如果我在列表中缓冲结果,我什么也得不到。我不妨使用不是懒惰的ReadAllLines()并返回一个字符串数组。如果要读取的文件非常大,则此操作将花费很长时间。在我可以访问数组(或列表)之前,我必须等待返回整个数组(或列表)的字符串。 - Adrian Constantin
@Adrian,如果你正在解析大文件,那么我会避免这种情况。 - bendewey
此方法的目的是能够获取IEnumerable,然后在Linq查询中使用迭代器。我不一定需要解析文件。我可能想从文件中获取数据并使用Linq查询对其进行转换或操作。 - Adrian Constantin


我不知道它是否可以被认为是一个错误,如果它是设计但我可以说两件事......

  1. 这应该发布在Connect上,而不是StackOverflow,尽管它们在4.0发布之前不会改变它。这通常意味着他们永远无法修复它。
  2. 该方法的设计肯定存在缺陷。

你是正确的指出返回一个IEnumerable意味着它应该是可重用的,如果迭代两次它不保证相同的结果。如果它返回了IEnumerator,那么它将是一个不同的故事。

所以无论如何,我认为这是一个很好的发现,我认为API是一个糟糕的开始。 ReadAllLines和ReadAllText为您提供了获取整个文件的一种非常方便的方法,但是如果调用者对使用惰性枚举的性能足够关注,那么他们不应该首先将这么多的责任委托给静态帮助器方法。


0
2018-02-21 01:26



IEnumerable并不意味着可重用性。它只意味着能够获得一个简单的枚举器。很多不可重用的前向IEnumerables都在框架中。还有其他接口适用于大多数可重用的对象,或提供的不仅仅是简单的枚举(例如IList)。 - Stephen M. Redd
我不同意。我小心不要说“保证”,因为它没有。但它确实如此 意味着 可重用性。甚至IEnumerator也意味着由于其Reset方法而具有可重用性。但是我希望多次调用IEnumerable.GetEnumerator不应该抛出或返回相同的实例,因为实际上每个其他IEnumerable的行为都是如此,包括LINQ查询。 - Josh


我相信你将IQueryable与IEnumerable混淆了。是的,IQueryable可以被视为IEnumerable,但它们并不完全相同。每次使用IQueryable查询,而IEnumerable没有这样隐含的重用。

Linq查询返回IQueryable。 ReadLines返回一个IEnumerable。

这里有一个微妙的区别,因为创建了枚举器的方式。当你在它上面调用GetEnumerator()时,IQueryable会创建一个IEnumerator(由foreach自动完成)。 ReadLines()在调用ReadLines()函数时创建IEnumerator。因此,当您重用IQueryable时,它会在您重用它时创建一个新的IEnumerator,但由于ReadLines()创建了IEnumerator(而不是IQueryable),因此获取新IEnumerator的唯一方法是再次调用ReadLines() 。

换句话说,您应该只能期望重用IQueryable而不是IEnumerator。

编辑:

在进一步思考(没有双关语)我认为我的初步反应有点过于简单化了。如果IEnumerable不可重用,则无法执行以下操作:

List<int> li = new List<int>() {1, 2, 3, 4};

IEnumerable<int> iei = li;

foreach (var i in iei) { Console.WriteLine(i); }
foreach (var i in iei) { Console.WriteLine(i); }

显然,人们不会指望第二个foreach失败。

这种抽象的问题往往是,并非一切都完美。例如,Streams通常是单向的,但对于网络使用,它们必须适应双向工作。

在这种情况下,最初设想IEnumerable是一个可重用的功能,但它已被改编为如此通用,以至于可重用性不是保证,甚至不应该是预期的。见证以不可重复使用的方式使用IEnumerables的各种库的爆炸式增长,例如Jeffery Richters PowerThreading库。

我根本不认为我们可以假设IEnumerables在所有情况下都可以重复使用。


0
2018-02-21 08:18



可能是这种情况,但MSDN上的文档(msdn.microsoft.com/en-us/library/dd383503(VS.100).aspx)没有明确指出你应该只迭代一次。在尝试修改正在迭代的集合的情况下尝试枚举时,可能会发生异常抛出。 - Adrian Constantin
@Adrian - 从什么时候开始我们查看了你不能做的文件?你通常会看着它 能够 做。文档本质上是不完整的,所以如果它告诉我们可以完成的话,我们通常会很幸运。如果它包含不能的东西,则往往更多的是注释。 - Erik Funkenbusch


这不是一个错误。 File.ReadLines()使用延迟评估,但不是 幂等。这就是为什么连续两次枚举它是不安全的。记住一个 IEnumerable 表示可以枚举的数据源,但它并未说明两次枚举是安全的,尽管这可能是意料之外的,因为大多数人习惯使用IEnumerable而不是幂等集合。

来自 MSDN

ReadLines(String,System)和   ReadAllLines(String,System)方法   区别如下:当你使用时   ReadLines,你可以开始枚举   之前的字符串集合   整个收藏归还;当你   使用ReadAllLines,你必须等待   返回整个字符串数组   在你可以访问之前   array.Therefore,因此,当你工作   对于非常大的文件,ReadLines可以   更有效率。

您通过反射器的发现是正确的,并验证此行为。您提供的实现避免了这种意外行为,但仍然使用延迟评估。


0
2018-02-21 09:23



这将是我见过的第一个也是唯一一个IEnumerable.GetEnumerator函数的例子,它不能被多次调用。 - Jonathan Allen
我们一直在对morelinq项目进行深入讨论,并决定将所有运算符实现为幂等。消费者自然会认为IEnumerables可以被多次枚举。同样,在这种情况下,它不是一个错误,它是一个功能。 - Johannes Rudolph
事实上,你不能枚举ReadLines(..)返回的IEnumerable的两倍,它只是一个实现细节。在枚举器的MoveNext()方法中抛出异常。我的实现使用reader作为局部变量,因此每次开始枚举时都会得到一个新的TextReader。显然,这里的问题是,一旦完成枚举,您需要一个新的TextReader。我没有看到为什么文件不会被多次迭代的原因。 - Adrian Constantin
@Jonathan Allen - 有很多枚举器的例子不能被多次枚举,尽管这是我在框架中看到的第一个。 IEnumerable习惯用法现在被用于整个地方,实际上是一个迷你状态引擎。有关其他示例,请参阅Jeffery Richters PowerThreading库。 - Erik Funkenbusch
当然,任何人都可以编写具有正确界面但以星形方式工作的东西。这就是为什么我把朦胧的目光转向不在框架内的那些。 - Jonathan Allen