CodeGym /Courses /C# SELF /Working with Byte Streams

Working with Byte Streams

C# SELF
Level 36, Lesson 4
Available

1. Introduction

We're already familiar with streams (Stream), which are basically an abstraction for sequentially reading or writing data. We've worked with FileStream to access files at the byte level, and also used StreamReader and StreamWriter for easy text handling, which under the hood use FileStream and take care of encodings.

But what if we need to store not just text in a file, but strictly typed data: integers (int), floating-point numbers (double, float), booleans (bool), dates (DateTime), or even custom structs? Sure, you could convert all that to strings and write it with StreamWriter, then parse it back when reading. But that approach has some serious downsides:

  • Inefficient storage: The number 12345 written as text takes up 5 bytes (characters). In binary, an int is just 4 bytes. For big data sets, that difference really matters.
  • Performance: Constantly converting numbers to strings and back is just extra CPU work.
  • Data accuracy: Converting floating-point numbers to text and back can lead to loss of precision due to rounding.
  • Parsing headaches: Manually splitting up text strings to extract different data types (like "123,45 TRUE 2024-06-21") makes your code way more complicated and fragile.

To solve these problems, there are special classes: BinaryReader and BinaryWriter. These are specialized adapters that work on top of any base Stream (usually FileStream) and give you handy methods for reading and writing C# primitive data types in their binary format. They handle all the byte-to-type conversions for you, making it way easier to work with structured binary files.

Key idea: BinaryReader and BinaryWriter aren't standalone streams. They enhance the functionality of an existing Stream by adding methods for working with C# types instead of just raw bytes.

2. Writing Data with BinaryWriter

BinaryWriter gives you a bunch of Write() methods, overloaded for each C# primitive type. When you call one of these, BinaryWriter converts the value to its binary representation (a sequence of bytes) and writes those bytes to the base Stream.

Example: Saving Game Settings

Let's say we want to save some game settings: volume level (a float), current player level (an int), whether music is on (a bool), and the selected difficulty (a string).


string filePath = "settings.bin";

// 1. Create a FileStream for writing
using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
// 2. Create a BinaryWriter on top of FileStream, specify encoding for strings (if any)
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);
// 3. Write different data types
writer.Write(0.75f);       // float (4 bytes)
writer.Write(15);          // int (4 bytes)
writer.Write(true);        // bool (1 byte)
writer.Write("Easy");      // string (length prefix + bytes)
                
Console.WriteLine($"Settings saved to '{filePath}'.");

Breaking down the example:

  • We create a FileStream with FileMode.Create, which makes a new file or overwrites an existing one.
  • Then we create a BinaryWriter, passing it fs. Important: BinaryWriter by default closes the base stream (fs) when you call its Dispose() method (which happens automatically with the using block).
  • The writer.Write() methods are super intuitive: Write(float), Write(int), Write(bool), Write(string). They know how many bytes to write for each type and how to represent them.
  • For strings, BinaryWriter automatically adds a length prefix before the actual string bytes. This lets BinaryReader know exactly how many bytes to read to reconstruct the string.
  • If you try to open settings.bin in a text editor, you'll just see "garbage" because it's a binary file. Use a HEX editor if you want to peek inside.

3. Reading Data with BinaryReader

BinaryReader gives you ReadXxx() methods (like ReadInt32(), ReadBoolean(), ReadString()) that read the right number of bytes from the base Stream and convert them to the needed C# data type.

Example: Loading Game Settings

Now let's read the settings from the settings.bin file we created earlier.


string filePath = "settings.bin";

// 1. Create a FileStream for reading
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// 2. Create a BinaryReader on top of FileStream, use the same encoding
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

// 3. Read data in THE SAME ORDER it was written
float volume = reader.ReadSingle();     // float
int level = reader.ReadInt32();         // int
bool isMusicOn = reader.ReadBoolean();  // bool
string difficulty = reader.ReadString(); // string
                
Console.WriteLine($"Settings loaded from '{filePath}':");
Console.WriteLine($"- Volume: {volume:P0}"); // Format as percent (using string formatting)
Console.WriteLine($"- Player level: {level}");
Console.WriteLine($"- Music on: {isMusicOn}");
Console.WriteLine($"- Difficulty: {difficulty}");

Breaking down the example:

  • We open a FileStream in FileMode.Open for reading.
  • Create a BinaryReader on top of fs, specifying the same encoding as when writing.
  • Super important: The order of reader.ReadXxx() calls must EXACTLY match the order in which the data was written with BinaryWriter. If you try to read a string where you wrote an int, you'll get an EndOfStreamException (if the string is longer) or just garbage data.
  • The ReadXxx() methods automatically read the right number of bytes and convert them to the requested type. ReadString() uses that length prefix written by BinaryWriter to know how many bytes to read for the whole string.

4. Important Nuances and Best Practices

Strict order:

This is the main rule. BinaryReader and BinaryWriter don't store any metadata about types; they just know how many bytes each primitive type takes. You have to make sure the order matches.

Resource management (using):

Like most .NET classes that work with system resources (like files or network connections), both BinaryReader and BinaryWriter implement the IDisposable interface. So always wrap them in a using block — that way, Dispose() is called automatically, even if something goes wrong. This protects you from leaks and properly closes the file.

By the way, by default BinaryWriter and BinaryReader will also call Dispose() on the base stream you give them (like FileStream), so that'll get closed automatically too.


using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);
// ... work

Encoding for strings:

To make sure strings written with BinaryWriter.Write(string) and read with BinaryReader.ReadString() work right, always specify the same encoding in their constructors (like Encoding.UTF8). Otherwise, you might get weird issues with non-ASCII characters.

Exception handling:

File I/O can always be interrupted by outside stuff (file missing, no permissions, disk full). Always wrap code with FileStream and BinaryReader/BinaryWriter in try-catch blocks for reliability.

BaseStream and position:

You can access the base stream via the BaseStream property (like reader.BaseStream or writer.BaseStream). This is handy if you want to know the current position (BaseStream.Position) or move around in the file (BaseStream.Seek()).


// Example using BaseStream.Position
using FileStream fs = new FileStream("data.bin", FileMode.OpenOrCreate);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(123);
Console.WriteLine($"Current position in stream: {writer.BaseStream.Position}"); // Will print 4 (size of int)

writer.Write("Hello");
Console.WriteLine($"Current position in stream: {writer.BaseStream.Position}"); // Will print 4 + (1+5) = 10

⚠️ The Write(string) method first writes the string length as a 7-bit integer, then the string bytes. So the final size isn't always just 1 + string length.

2
Task
C# SELF, level 36, lesson 4
Locked
Writing and Reading Integers Using BinaryWriter and BinaryReader
Writing and Reading Integers Using BinaryWriter and BinaryReader
1
Survey/quiz
Input-Output Streams, level 36, lesson 4
Unavailable
Input-Output Streams
Reading and Writing Files
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION