C# Streams, StreamReader, and StreamWriter Explained
In C# and the .NET runtime, input and output (I/O) operations rely on
streams to transfer data between memory, files, and network sockets.
This article explains the architectural differences and use cases
between the base Stream class, which handles raw binary
byte sequences, and the helper classes StreamReader and
StreamWriter, which abstract character encoding to process
human-readable text.
The Foundation: The Stream Class
The System.IO.Stream class is an abstract base class
that represents a contiguous sequence of bytes. It provides a generic
view of a sequence of bytes, regardless of the underlying backing store
(such as a file, memory buffer, or network connection).
Common implementations include:
FileStream: Reads and writes bytes to and from physical files.MemoryStream: Reads and writes bytes to and from system memory as a backing store.NetworkStream: Facilitates byte transfer across network sockets.
Streams provide core methods such as Read(),
Write(), Seek(), and Flush().
Because they operate strictly on byte arrays (byte[]), raw
streams are primarily used for binary I/O—tasks such as
transmitting raw packets, reading serialized binary formats, image
manipulation, or parsing compressed archives.
Binary I/O with Streams
When performing binary operations, you work with raw bytes without
encoding conversions. To simplify reading and writing primitive data
types (such as int, bool, or
double) to a binary stream, .NET provides
BinaryReader and BinaryWriter. These classes
wrap an existing Stream object and read or write primitive
values directly as binary data, preserving the exact bit-level
representation.
Text I/O: StreamReader and StreamWriter
While raw streams manipulate bytes, human-readable text consists of characters and strings. Because a single character can span anywhere from one to four bytes depending on the encoding (e.g., UTF-8, UTF-16, ASCII), handling text via raw byte arrays is error-prone.
StreamReader and StreamWriter solve this
problem by wrapping an underlying Stream and automatically
translating between byte representations and C# char or
string types.
StreamReader
StreamReader inherits from TextReader and
reads characters from a byte stream using a specified character encoding
(defaulting to UTF-8).
- Use Case: Reading log files, configuration files, CSVs, or text payloads.
- Key Methods:
ReadLine(): Reads a single line of characters and returns it as astring.ReadToEnd(): Reads all characters from the current position to the end of the stream.Read(): Reads the next single character or a block of characters.
using (FileStream fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read))
using (StreamReader reader = new StreamReader(fs))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
// Process each line of text
}
}StreamWriter
StreamWriter inherits from TextWriter and
writes characters and strings to a byte stream, converting them into
bytes using the chosen encoding.
- Use Case: Generating text files, writing structured logs, or formatting plain text output.
- Key Methods:
Write(): Writes text without appending a line terminator.WriteLine(): Writes text followed by the environment-specific line terminator.Flush(): Clears all buffers and causes buffered data to be written to the underlying stream.
using (FileStream fs = new FileStream("output.txt", FileMode.Create, FileAccess.Write))
using (StreamWriter writer = new StreamWriter(fs))
{
writer.WriteLine("Hello, World!");
writer.WriteLine("Second line of text.");
}Key Differences Between Binary and Text I/O
| Feature | Streams / BinaryReader / BinaryWriter | StreamReader / StreamWriter |
|---|---|---|
| Data Unit | Bytes (byte,
byte[]) and binary primitives |
Characters (char,
string) |
| Encoding | None; processes raw bytes directly | Requires character encoding (UTF-8, ASCII, etc.) |
| Performance | Faster; zero overhead from string parsing | Minor overhead due to decoding/encoding characters |
| Target Data | Images, compiled files, network packets, binary formats | JSON, XML, CSV, plain text documents, logs |
Resource Management
All stream-related classes implement the IDisposable
interface. Wrapping them in using statements ensures that
unmanaged operating system handles, file locks, and network sockets are
closed and released immediately when the operation concludes, while also
ensuring that buffered output data is properly flushed. Closing an outer
reader or writer automatically closes the underlying stream it
wraps.