C#中的FileStream和StreamReader问题

23

我正在测试FileStream和StreamReader类如何共同使用,使用控制台应用程序。 我试图打开文件并读取行并将它们打印到控制台。

我已经使用while循环做到了这一点,但我想尝试使用foreach循环。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace testing
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string file = @"C:\Temp\New Folder\New Text Document.txt";
            using(FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read))
            {
                using(StreamReader sr = new StreamReader(fs))
                {
                    foreach(string line in file)
                    {
                        Console.WriteLine(line);
                    }
                }
            }
        }
    }
}

我一直收到的错误是:无法将'type char'转换为'type string'

这个能正常运行的while循环看起来像这样:

while((line = sr.ReadLine()) != null)
{
    Console.WriteLine(line);
}

我可能忽略了一些非常基础的东西,但是我看不到它。


关于foreach(直接与您关于“yield”的评论相关),我建议阅读《C#深度剖析》的免费第6章 - 在这里:http://www.manning.com/skeet/ - Marc Gravell
你的问题在于你的代码正在遍历(foreach)“file”中的每个元素(它是一个字符串)。因此,每个元素都是一个“char”。因此编译器错误消息指出你试图将其转换为字符串类型。你应该遍历(foreach)流中的数据。 - RichS
10个回答

44

如果你想以可重用的方式通过foreach逐行读取文件,请考虑以下迭代器块:

    public static IEnumerable<string> ReadLines(string path)
    {
        using (StreamReader reader = File.OpenText(path))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                yield return line;
            }
        }
    }

请注意,这是一种惰性求值的方式——与File.ReadAllLines()相关的缓冲区不存在。使用foreach语法可以确保即使出现异常也能正确地Dispose()迭代器并关闭文件:
foreach(string line in ReadLines(file))
{
    Console.WriteLine(line);
}

(这部分内容仅供参考...)

这种类型的抽象化还有一个优点是,它与LINQ非常契合-也就是说,用这种方法进行转换/筛选等处理非常容易:

        DateTime minDate = new DateTime(2000,1,1);
        var query = from line in ReadLines(file)
                    let tokens = line.Split('\t')
                    let person = new
                    {
                        Forname = tokens[0],
                        Surname = tokens[1],
                        DoB = DateTime.Parse(tokens[2])
                    }
                    where person.DoB >= minDate
                    select person;
        foreach (var person in query)
        {
            Console.WriteLine("{0}, {1}: born {2}",
                person.Surname, person.Forname, person.DoB);
        }

再次强调,所有内容都是惰性评估的(没有缓冲)。


1
自从 .net 4.0 版本以来,File.ReadLines 已经作为 BCL 的一部分内置了。 - CodesInChaos
使用 while (!reader.EndOfStream) { yield return reader.ReadLine() } 是否在性能上稍微更好一些呢?显然并不会有太大的差距,但是上面的变量 line 除了在 yield return 外没有任何用处,我个人认为这种方式更易读。 - cogumel0

27

读取 New Text Document.txt 中的所有行:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace testing
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string file = @"C:\Temp\New Folder\New Text Document.txt";
            using(FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read))
            {                    
                using(StreamReader sr = new StreamReader(fs))
                {
                    while(!sr.EndOfStream)
                    {
                       Console.WriteLine(sr.ReadLine());
                    }
                }
            }
        }
    }
}

26

我在我的MiscUtil项目中有一个LineReader类。它比这里提供的解决方案稍微更通用,主要体现在构造方式上:

  • 从返回流的函数中构造,此时将使用UTF-8编码
  • 从返回流和编码的函数中构造
  • 从返回文本读取器的函数中构造
  • 仅从文件名构造,此时将使用UTF-8编码
  • 从文件名和编码构造

该类“拥有”其使用的任何资源,并适当关闭它们。不过,它没有实现IDisposable接口。这就是为什么它需要以Func<Stream>Func<TextReader>的形式接受流或读取器的函数 - 它需要能够推迟打开操作直到需要时再进行。遍历器本身(由foreach循环自动处理)会关闭资源。

正如Marc所指出的那样,这在LINQ中非常有效。我喜欢给出一个示例:

var errors = from file in Directory.GetFiles(logDirectory, "*.log")
             from line in new LineReader(file)
             select new LogEntry(line) into entry
             where entry.Severity == Severity.Error
             select entry;

这将从许多日志文件中流式传输所有错误,随着它的进行打开和关闭。结合Push LINQ,您可以做各种好玩的事情 :)

这不是一个特别“棘手”的类,但非常方便。以下是完整的源代码,如果您不想下载MiscUtil,可以方便地使用。源代码的许可证在这里

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace MiscUtil.IO
{
    /// <summary>
    /// Reads a data source line by line. The source can be a file, a stream,
    /// or a text reader. In any case, the source is only opened when the
    /// enumerator is fetched, and is closed when the iterator is disposed.
    /// </summary>
    public sealed class LineReader : IEnumerable<string>
    {
        /// <summary>
        /// Means of creating a TextReader to read from.
        /// </summary>
        readonly Func<TextReader> dataSource;

        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched. UTF-8 is used to decode
        /// the stream into text.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        public LineReader(Func<Stream> streamSource)
            : this(streamSource, Encoding.UTF8)
        {
        }

        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        /// <param name="encoding">Encoding to use to decode the stream
        /// into text</param>
        public LineReader(Func<Stream> streamSource, Encoding encoding)
            : this(() => new StreamReader(streamSource(), encoding))
        {
        }

        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// UTF8 is used to decode the file into text.
        /// </summary>
        /// <param name="filename">File to read from</param>
        public LineReader(string filename)
            : this(filename, Encoding.UTF8)
        {
        }

        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// </summary>
        /// <param name="filename">File to read from</param>
        /// <param name="encoding">Encoding to use to decode the file
        /// into text</param>
        public LineReader(string filename, Encoding encoding)
            : this(() => new StreamReader(filename, encoding))
        {
        }

        /// <summary>
        /// Creates a LineReader from a TextReader source. The delegate
        /// is only called when the enumerator is fetched
        /// </summary>
        /// <param name="dataSource">Data source</param>
        public LineReader(Func<TextReader> dataSource)
        {
            this.dataSource = dataSource;
        }

        /// <summary>
        /// Enumerates the data source line by line.
        /// </summary>
        public IEnumerator<string> GetEnumerator()
        {
            using (TextReader reader = dataSource())
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    yield return line;
                }
            }
        }

        /// <summary>
        /// Enumerates the data source line by line.
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

不错!你应该把这个放到GitHub上啊,伙计。 - Fandango68

3
以下更为优雅:

稍微更为优雅的是以下内容...

using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read))
{
    using (var streamReader = new StreamReader(fileStream))
    {
        while (!streamReader.EndOfStream)
        {
            yield return reader.ReadLine();
        }
    }
}

3
问题出在:
foreach(string line in file)
{
    Console.WriteLine(line);
}

因为“file”是字符串,而字符串实现了IEnumerable接口。但是该枚举器返回的是“char”,而“char”不能隐式转换为字符串。
应该使用while循环,正如你所说的那样。

1

看起来像是作业;)

你正在迭代文件名(一个字符串)本身,这会一次给你一个字符。只需使用正确使用sr.ReadLine()的while方法即可。


这有点像作业。我正在做另一件包含类似内容的事情。所以我正在逐渐了解FileStream和StreamReader/StreamWriter类 ^^ - Vordreller

1

不必使用 StreamReader 然后尝试在 String file 变量中查找行,您可以直接使用 File.ReadAllLines

string[] lines = File.ReadAllLines(file);
foreach(string line in lines)
   Console.WriteLine(line);

如果您不想锁定文件,并且文件比内存大得多,则无需这样做! - Fandango68

0
一个简单(但不是内存高效)的方法是迭代文件中的每一行。
foreach (string line in File.ReadAllLines(file))
{
  ..
}

请查看我的帖子,以获取一个非缓冲版本的内容。 - Marc Gravell

0

你正在枚举一个字符串,每次只取一个字符。

你确定这是你想要的吗?

foreach(string line in file)

不是我真正想要的,我只是在测试可能性。实际上我很少使用foreach循环。 - Vordreller

0

我猜你想要这样的东西:

using ( FileStream fileStream = new FileStream( file, FileMode.Open, FileAccess.Read ) )
{
    using ( StreamReader streamReader = new StreamReader( fileStream ) )
    {
        string line = "";
        while ( null != ( line = streamReader.ReadLine() ) )
        {
            Console.WriteLine( line );
        }
    }
}

1
这正是我在第一篇帖子中打印的内容,只是StreamReader的指针名称不同,并且while循环的另一侧有一个!= null... - Vordreller
1
不完全是这样,因为我的代码可以运行,而你的则不能。;-)你的问题在于你正在对文件名进行“foreach”操作,而不是对流进行操作。 - RichS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接