FileStream StreamReader problem in C#
Asked Answered
R

10

23

I'm testing how the classes FileStream and StreamReader work togheter. Via a Console application. I'm trying to go in a file and read the lines and print them on the console.

I've been able to do it with a while-loop, but I want to try it with a foreach loop.

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

The error I keep getting for this is: Cannot convert type 'char' to 'string'

The while loop, which does work, looks like this:

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

I'm probably overlooking something really basic, but I can't see it.

Rotl answered 13/11, 2008 at 8:38 Comment(2)
On the subject of foreach (directly relating to your comment about "yield"), I recommend the free chapter 6 of C# in Depth - here: manning.com/skeetStonechat
Your problem is that your code is iterating (foreach) through each element in "file" (which is a string). Therefore each element is a "char". Hence the compiler error message that you are then trying to convert to the string type. You should be iterating (foreach) through the data from the stream.Kauslick
M
27

To read all lines in 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());
                    }
                }
            }
        }
    }
}
Mainly answered 13/11, 2008 at 8:47 Comment(0)
S
44

If you want to read a file line-by-line via foreach (in a reusable fashion), consider the following iterator block:

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

Note that this this is lazily evaluated - there is none of the buffering that you would associate with File.ReadAllLines(). The foreach syntax will ensure that the iterator is Dispose()d correctly even for exceptions, closing the file:

foreach(string line in ReadLines(file))
{
    Console.WriteLine(line);
}

(this bit is added just for interest...)

Another advantage of this type of abstraction is that it plays beautifully with LINQ - i.e. it is easy to do transformations / filters etc with this approach:

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

And again, all evaluated lazily (no buffering).

Stonechat answered 13/11, 2008 at 8:46 Comment(2)
Since .net 4.0 this is built into the BCL as File.ReadLinesTommi
wouldn't using while (!reader.EndOfStream) { yield return reader.ReadLine() } be slightly better performance-wise? Obviously not by a long margin at all, but the variable line above is used nowhere other than on the yield return, and I personally find the condition more readable this way around.Evangelist
M
27

To read all lines in 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());
                    }
                }
            }
        }
    }
}
Mainly answered 13/11, 2008 at 8:47 Comment(0)
R
26

I have a LineReader class in my MiscUtil project. It's slightly more general than the solutions given here, mostly in terms of the way you can construct it:

  • From a function returning a stream, in which case it will use UTF-8
  • From a function returning a stream, and an encoding
  • From a function which returns a text reader
  • From just a filename, in which case it will use UTF-8
  • From a filename and an encoding

The class "owns" whatever resources it uses, and closes them appropriately. However, it does this without implementing IDisposable itself. This is why it takes Func<Stream> and Func<TextReader> instead of the stream or the reader directly - it needs to be able to defer the opening until it needs it. It's the iterator itself (which is automatically disposed by a foreach loop) which closes the resource.

As Marc pointed out, this works really well in LINQ. One example I like to give is:

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;

This will stream all the errors from a whole bunch of log files, opening and closing as it goes. Combined with Push LINQ, you can do all kinds of nice stuff :)

It's not a particularly "tricky" class, but it's really handy. Here's the full source, for convenience if you don't want to download MiscUtil. The licence for the source code is here.

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();
        }
    }
}
Revolting answered 13/11, 2008 at 9:13 Comment(1)
Nice! You should put this up in GitHub mate.Ecru
K
3

The problem is in:

foreach(string line in file)
{
    Console.WriteLine(line);
}

Its because the "file" is string, and string implements IEnumerable. But this enumerator returns "char" and "char" can not be implictly converted to string.

You should use the while loop, as you sayd.

Kyrstin answered 13/11, 2008 at 8:41 Comment(0)
H
3

Slightly more elegant is the following...

using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read))
{
    using (var streamReader = new StreamReader(fileStream))
    {
        while (!streamReader.EndOfStream)
        {
            yield return reader.ReadLine();
        }
    }
}
Henrique answered 7/12, 2010 at 4:56 Comment(0)
E
1

Looks like homework to me ;)

You're iterating over the filename (a string) itself which gives you one character at a time. Just use the while approach that correctly uses sr.ReadLine().

Editorialize answered 13/11, 2008 at 8:44 Comment(1)
It's 'kinda' homework. I'm working on something else that contains similar things. So I'm getting to know the FileStream and StreamReader/StreamWriter classes a bit ^^Rotl
M
1

Instead of using a StreamReader and then trying to find lines inside the String file variable, you can simply use File.ReadAllLines:

string[] lines = File.ReadAllLines(file);
foreach(string line in lines)
   Console.WriteLine(line);
Macedonia answered 13/11, 2008 at 8:45 Comment(1)
Not if you're not wanting to lock the file, and the file is way bigger than ram!Ecru
M
0

You are enumerating a string, and when you do that, you take one char at the time.

Are you sure this is what you want?

foreach(string line in file)
Maestricht answered 13/11, 2008 at 8:42 Comment(1)
Not really what I want, I'm just testing the possibilities. I've never used the foreach-loop much really.Rotl
E
0

A simplistic (not memory efficient) approach of iterating every line in a file is

foreach (string line in File.ReadAllLines(file))
{
  ..
}
Editorialize answered 13/11, 2008 at 8:46 Comment(1)
See my post for a non-buffering version of thisStonechat
K
0

I presume you want something like this:

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 );
        }
    }
}
Kauslick answered 13/11, 2008 at 8:47 Comment(2)
That's exactly what I printed in my first post, just a different pointername for the StreamReader and the != null on the other side of the while loop...Rotl
Well not quite, as mine works, yours doesn't. ;-) Your problem is that you are doing a "foreach" on the name of the file, not on the stream.Kauslick

© 2022 - 2024 — McMap. All rights reserved.