C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
200: FileStream In Depth
Listen, you've probably used File.ReadAllText or File.WriteAllLines by now. Those are great for quick config files or small logs, but they're "all-or-nothing" operations. They load the entire file into your RAM. If you're dealing with a 2GB game save file or a massive binary database, your app is going to crash before it even finishes reading the header.
I've got a binary file here called player.dat. It's a mess of bytes, but I know from the documentation that the player's "Experience Points" are stored as a 4-byte integer starting exactly at the 12th byte of the file. I want to read just those 4 bytes, update them, and save them back without touching the rest of the file.
The "Load Everything" Trap
My first instinct—the lazy one—is usually just to grab the whole byte array. Let's see what happens when I try to do this the simple way:
byte[] data = File.ReadAllBytes("player.dat");
// I know XP is at index 12
int xp = BitConverter.ToInt32(data, 12);
Console.WriteLine($"Current XP: {xp}");
This works. For a small file, it's fine. But here's the problem: if I want to change that XP value, I have to write the entire array back to disk using File.WriteAllBytes. I'm rewriting 100MB of data just to change 4 bytes. That's a waste of IO and an invitation for file corruption if the power cuts out mid-write. We need a scalpel, not a sledgehammer.
Slicing into the Binary
This is where FileStream comes in. It doesn't load the file; it opens a "pipe" to it. I can move a cursor (the position) to exactly where I want to be and read only what I need.
Let's try opening the stream and reading that XP value:
using (FileStream fs = new FileStream("player.dat", FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[4];
fs.Position = 12; // Jump straight to the XP offset
fs.Read(buffer, 0, 4);
int xp = BitConverter.ToInt32(buffer, 0);
Console.WriteLine($"XP found at position 12: {xp}");
}
Notice the fs.Position = 12. I'm telling the OS, "Don't bother with the first 11 bytes, just put the cursor here." The Read method then pulls the next 4 bytes into my buffer and advances the cursor to position 16. It's incredibly efficient because the memory footprint is just 4 bytes, regardless of how huge the file is.
Jumping and Overwriting
Now, let's say we want to give the player a bonus. I don't want to rewrite the file; I just want to overwrite those 4 bytes. To do that, I need to change my FileAccess and FileMode.
I'll try this, but I have to be careful. If I use FileMode.Create, I'll wipe the whole file. I need FileMode.Open.
using (FileStream fs = new FileStream("player.dat", FileMode.Open, FileAccess.ReadWrite))
{
// Move to the XP spot
fs.Position = 12;
int newXp = 5000;
byte[] xpBytes = BitConverter.GetBytes(newXp);
// Write only the 4 bytes for XP
fs.Write(xpBytes, 0, xpBytes.Length);
}
I just performed a "surgical strike" on the file. I opened it, jumped to byte 12, swapped the data, and closed it. The rest of the file remains untouched. One thing I've learned the hard way: always use that using block. If you don't dispose of the FileStream, the OS keeps a lock on the file. I've spent far too many hours wondering why I couldn't delete a file in Windows, only to realize my debug session was still holding the stream open.
Dealing with the Buffer
What if I don't know exactly where the data is? What if I'm searching for a "Magic Marker" (a specific sequence of bytes) that signals the start of a data block? I can't just jump; I have to scan.
I'll try reading in chunks. Reading one byte at a time is slow because every Read call is a request to the operating system. Instead, I'll use a larger buffer to bring a "chunk" of the file into memory, scan it, and move on.
using (FileStream fs = new FileStream("player.dat", FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[4096]; // Read 4KB at a time
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// Search through the buffer for my marker (e.g., 0xAA 0xBB)
for (int i = 0; i < bytesRead - 1; i++)
{
if (buffer[i] == 0xAA && buffer[i+1] == 0xBB)
{
Console.WriteLine($"Marker found at position: {fs.Position - bytesRead + i}");
}
}
}
}
By reading 4KB blocks, I'm reducing the number of system calls significantly. This is the core of how high-performance file parsing works: minimize the trips to the disk, maximize the work you do with the data once it's in your RAM.
📋 Practical Task
Exercise: The Binary Header Patch Tool
You are tasked with creating a tool that modifies the "Version Number" of a custom binary file. The file format specifies that the version is a 2-byte integer (UInt16) located at the very beginning of the file (offset 0). However, the file also has a "Checksum" byte at the very end of the file that must be incremented by 1 every time the version is changed.
Your Task: Write a program that does the following:
- Opens a file named
app.bin(you can create this file manually with any random bytes first). - Reads the first 2 bytes to display the current version.
- Updates the first 2 bytes to a new version number (e.g.,
2). - Seeks to the very last byte of the file.
- Reads that last byte, increments its value by 1, and writes it back.
- Ensures the stream is properly closed, even if an error occurs during the process.
There are no comments for now.