C# Streams Explained with Real-Life Analogies

Loc Le - Sep 9 - - Dev Community

Have you ever wondered how data flows through your programs, like how water moves from one place to another? That’s where streams come in. In C#, a stream represents the flow of data, typically as a sequence of bytes, whether it’s from a file, a network connection, or even memory. Enter streams, an efficient way to handle data flow, just like using a bucket to scoop water from a deep well. Let’s dive deeper into this concept with some easy-to-understand analogies.


What is a Stream in C#?

According to microsoft’s documentation, a stream is an abstraction of a sequence of bytes, allowing you to read, write, or move through data. It handles the flow of data efficiently without requiring you to load everything into memory. When working with streams, you’ll primarily perform three operations:

  • Read: Reading data from the stream
  • Write: Writing data to the stream
  • Seek: Moving a cursor to navigate through the data

Analogy

We can think of a stream like a glass tank of water, you can do various things with it, just like with a stream:

  • Read: You can inspect the water inside the tank to understand what’s there.
  • Write: You can add more water to the tank.
  • Seek: You can dive to a specific depth to pick up exactly the water you need.

C# Streams


Why Use Streams?

Let’s say you’re working with a deep well (a huge file) that contains a vast amount of water (data). Your task is to get a particular sample of water from the bottom for an experiment. Now, you have two choices:

Option 1: Pump All the Water to the Surface

You could use a pump to bring all the water from the well into a big tank before getting the sample you need. But there are downsides:

  • You need a massive tank to hold all the water (large memory usage).
  • It takes a long time to pump up all that water (slow processing time).

Option 2: Use a Professional Diver

A better approach would be to hire a diver (the stream’s cursor) with a small box (buffer). The diver will head straight to the bottom, grab the sample you need, and bring it back up:

  • The box (buffer) only holds a small portion of water (minimal memory usage).
  • The diver (cursor) goes exactly where you tell them, saving time and resources.

Naturally, we prefer the second option: using a stream. It’s much more efficient because it only processes the data you need, exactly when you need it, without loading everything into memory at once.


How Streams Work

Streams use an internal cursor to track the current position as you read or write data. Here’s how it works:

  1. The cursor moves through the stream, acting as your guide.
  2. Once the cursor reaches the desired position, you can start reading or writing data from that point using a buffer (or even byte by byte).

In this way, streams allow you to access large files or data sources in manageable chunks, without overwhelming memory or slowing down performance.

How streams work


Moving Water with Streams: A Practical Example

Now, let’s imagine a scenario where you need to move water (data) from one well (source file) to a water tank (target file). Here’s how you’d use streams to get the job done:

  1. First, set up a stream to read data (water) from the source well.
  2. As the cursor moves through the stream, read a small portion of data (water) into a buffer (bucket).
  3. You can now process that buffer — filtering the water or adding minerals, just as you might transform data.
  4. Finally, write that processed buffer into the target stream (the water tank).

Stream water analogy

This approach lets you efficiently move data from one place to another, even when dealing with large files.

C# code sample

using System.Text;

// Declare paths of well and water tank
string wellPath = "well.txt";
string waterTankPath = "water-tank.txt";

// Well of water is like a source of water stream is flowing
using FileStream well = new FileStream(wellPath, FileMode.Open);

// Water tank is like a destination where water is stored
using FileStream waterTank = new FileStream(waterTankPath, FileMode.Create);

// Buffer is like a bucket that carries water from well to water tank
byte[] buffer = new byte[2048];

// Read water from well and write to water tank
// Using buffer to carry water from well to water tank
while (well.Read(buffer, 0, buffer.Length) > 0)
{
    // Get pure water from bucket
    string pureWater = Encoding.UTF8.GetString(buffer);

    // Processed water is like a water with minerals
    string processedWater = pureWater.Replace("Water", "Water with minerals");

    // Store processed water in bucket
    buffer = Encoding.UTF8.GetBytes(processedWater);

    // Write processed water to water tank
    waterTank.Write(buffer, 0, buffer.Length);
}
Enter fullscreen mode Exit fullscreen mode

Q&A Section

Do I Need to Use a Buffer (byte[])?

Not necessarily! You can read or write data byte by byte if you’d like, similar to picking up a drop of water at a time. However, this might not be the most efficient way.

If Buffers Are Optional, Why Use Them?

  • Performance: Processing larger chunks of data at once (like using a bucket instead of picking up single drops) is much faster.
  • Memory Management: Declaring a buffer means you’re specifying how much memory to use, allowing you to control and optimize resource usage.

In summary, streams in C# are a powerful tool for handling data efficiently, especially when working with large files. By breaking data into smaller chunks and processing them as needed, streams help keep memory usage low while improving performance. Hopefully, these analogies give you a clearer understanding of how streams work — and why they’re so useful!

.
Terabox Video Player