A stream is a byte sequence with capabilities
A stream exposes byte operations and advertises which operations its implementation supports. The lesson uses MemoryStream, whose cursor and length are easy to observe without accessing a filesystem. Length describes the extent of the sequence; Position describes where the next operation starts. Neither property is a count of characters, records or successful reads. A write normally moves the cursor forward, so writing a message does not automatically prepare that same object for reading from its beginning. This distinction becomes especially important when a producer hands an already populated stream to a consumer.
Follow the five printed observations
The first write places the decimal bytes ten, twenty and thirty into the stream. Its length and position both become three, producing 3:3. The next ReadByte begins at that position and returns minus one. Assigning zero to Position deliberately rewinds the cursor. Reading into a two-byte buffer returns two and moves the cursor to two, which explains 2:2. A subsequent ReadByte returns thirty, and the final call returns minus one again. Every printed value follows from one operation and its starting cursor; no read silently rewinds or consumes an entire logical message.
Use the correct end marker for the operation
The single-byte API returns an integer so it can represent all byte values as well as its negative end marker. The block API instead returns how many bytes were copied. For a nonzero-length read request, a returned count of zero indicates end-of-input in this synchronous contract. An empty destination can return zero without testing whether data remains. A positive result smaller than the requested capacity is still progress, as the next lesson demonstrates. Keep these contracts separate rather than testing a returned byte against zero, since zero is a perfectly valid byte in binary data.
Seeking is a capability, not a universal stream feature
This example can assign Position because MemoryStream supports seeking. A different implementation may reject Position, Length or Seek with NotSupportedException. A compression stream or network body should usually be consumed sequentially rather than forced into a cursor model copied from a memory exercise. Inspect CanRead, CanWrite and CanSeek where an algorithm actually needs those capabilities, and document the requirement. Do not infer that a stream is rewindable merely because its concrete contents were ultimately produced from an array somewhere earlier in a larger pipeline.
Separate stream state from ownership and text
A rewind changes only this stream's cursor. It does not dispose a wrapper, flush a still-active text writer, decode an encoding, or prove that another reader has no buffered data. In the sample there are no wrappers and one local owner, making the observations deliberately narrow. A using declaration disposes the memory stream at the end of the scope even when an exception exits early. The same ownership structure is useful for external resources, but this lab itself opens no file and sends no request. Its deterministic output describes byte state rather than operating-system behavior.
Make two predictions before running changes
For the first independent exercise, change only the position assigned before the block read from zero to one. Draw the remaining byte sequence and predict each later print, including the cursor after the read. For the second, keep the original rewind but replace the two-byte destination with a four-byte destination. Predict the returned count separately from the array's capacity and identify what the next ReadByte observes. These exercises change different assumptions: the first changes where consumption begins, while the second changes how much capacity the consumer offers.
- Hint for position one: the read consumes twenty and thirty, leaving position three.
- Hint for capacity four: only three source bytes exist; unused array slots are not additional input.
Explain the state without relying on convenience behavior
A useful review can reconstruct the original output without executing the program: write moves to three; end is observed; explicit rewind permits two bytes; one byte remains; end is observed again. Then describe how the algorithm would change for a stream that cannot seek: a caller must provide a correctly positioned input, or the producer must return a fresh readable sequence. Avoid adding an unconditional rewind inside a reusable consumer, because that changes a caller's intended starting point and rejects valid sequential streams. State positioning requirements as part of the method contract instead.
- Keep byte values, byte counts, stream length and buffer capacity as separate quantities.
- Qualify zero-count end checks with a nonempty read request.
- Use seeking only when the consumer explicitly requires and supports it.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.IO;
using System.IO.Compression;
using System.Buffers.Binary;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using var stream = new MemoryStream();
stream.Write(new byte[] { 10, 20, 30 });
Console.WriteLine($"{stream.Length}:{stream.Position}");
Console.WriteLine(stream.ReadByte());
stream.Position = 0;
byte[] buffer = new byte[2]; int count = stream.Read(buffer, 0, buffer.Length);
Console.WriteLine($"{count}:{stream.Position}");
Console.WriteLine(stream.ReadByte()); Console.WriteLine(stream.ReadByte());
Expected output
3:3 -1 2:2 30 -1
Read from the current cursor, and distinguish ReadByte’s -1 from a block Read count of zero.