Fundamentals – Part 11

10/10/2025
4 minute read

Span, Memory, and ReadOnly Variants

Working with data efficiently often comes down to how you manage memory. Traditionally, arrays and collections require allocating and copying data, which can be expensive in performance-critical scenarios like parsing, serialization, or networking.

C# introduced Span, Memory, and their readonly counterparts to enable high-performance, allocation-free access to data — both on the stack and the heap.

Let’s explore what these types do, how they differ, and where ReadOnlySequence<T> fits in.


Span

Span<T> is a stack-only type that represents a contiguous region of memory. It can reference an array, a slice of an array, or even unmanaged memory — without allocating a new array.

int[] numbers = { 10, 20, 30, 40, 50 };
// Create a Span that points to part of the array (indexes 1–3)
Span<int> middle = numbers.AsSpan(1, 3);
// Modify through the Span
middle[0] = 99;    // changes numbers[1]
middle[2] = 77;    // changes numbers[3]

// Print results Console.WriteLine(string.Join(", ", numbers)); // Output: 10, 99, 30, 77, 50

Key points:

  • Span<T> is a ref struct, meaning it lives only on the stack.
  • It cannot be captured by lambdas, stored in fields, or boxed.
  • It’s zero-allocation, making it ideal for slicing or manipulating large data buffers.

ReadOnlySpan

ReadOnlySpan<T> works the same way as Span<T>, but it provides read-only access. You can create it from arrays, strings, or other buffers — perfect for scenarios where safety matters.

ReadOnlySpan<char> span = "Hello, world!".AsSpan();
Console.WriteLine(span.Slice(0, 5).ToString()); // Hello

This is how APIs like ReadOnlySpan<byte> power high-performance text processing, I/O, and protocol parsing without copying data.


Memory

Memory<T> is similar to Span<T>, but it can live on the heap instead of the stack.
That means you can store it in fields, pass it between methods, or use it in async code, which you can’t do with Span<T>.

public static async Task ProcessDataAsync(Memory<int> memory)
{
    // Access the data through memory.Span
    var span = memory.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] *= 2; // Double every value
    // Simulate async I/O
    await Task.Delay(100);
    Console.WriteLine("Processed data: " + string.Join(", ", span.ToArray()));
}
public static async Task Main()
{
    int[] data = { 1, 2, 3, 4, 5 };
    Memory<int> memory = data; // wrap the array
    await ProcessDataAsync(memory);
    Console.WriteLine("Original array: " + string.Join(", ", data));
}
//Output
Processed data: 2, 4, 6, 8, 10
Original array: 2, 4, 6, 8, 10

What’s happening here?

  • Memory<T> wraps the original array without copying it.
  • Inside ProcessDataAsync, we access its contents using .Span.
  • Because Memory<T> can live on the heap, it’s safe to use in async methods or store in fields.

So the key distinction is:

  • Span<T> → fast, stack-only, short-lived.
  • Memory<T> → heap-safe, async-friendly, longer-lived.

ReadOnlyMemory

Like ReadOnlySpan<T>, this version enforces read-only access — but unlike ReadOnlySpan<T>, it can live on the heap.

ReadOnlyMemory<char> readOnlyMemory = "CSharp".AsMemory();
Console.WriteLine(readOnlyMemory.Span[2]); // a

These types are used extensively in APIs like System.IO.Pipelines and System.Buffers, where high-performance, memory-safe code is crucial.


ReadOnlySequence

ReadOnlySequence<T> represents a sequence of memory segments — not necessarily contiguous in memory.
It’s often used with PipeReader and System.IO.Pipelines, where data arrives in chunks. Example (simplified):

using System.Buffers;
var first = new ReadOnlyMemory<byte>(new byte[] { 1, 2, 3 });
var second = new ReadOnlyMemory<byte>(new byte[] { 4, 5 });
var sequence = CreateSequence(first, second);
foreach (var segment in sequence)
{
    foreach (var b in segment.Span)
        Console.Write($"{b} ");
}
// Output: 1 2 3 4 5
static ReadOnlySequence<byte> CreateSequence(ReadOnlyMemory<byte> first, ReadOnlyMemory<byte> second)
{
    var segment1 = new BufferSegment(first);
    var segment2 = new BufferSegment(second);
    segment1.SetNext(segment2);
    return new ReadOnlySequence<byte>(segment1, 0, segment2, second.Length);
}
class BufferSegment : ReadOnlySequenceSegment<byte>
{
    public BufferSegment(ReadOnlyMemory<byte> memory) => Memory = memory;
    public void SetNext(BufferSegment next)
    {
        Next = next;
        next.RunningIndex = RunningIndex + Memory.Length;
    }
}

ReadOnlySequence<T> is how .NET handles streaming, chunked data efficiently, without copying or flattening buffers.

When to Use What

Type Stack or Heap Mutable Lifetime Use Case
Span Stack Short-lived Fast slicing or stack buffers
ReadOnlySpan Stack Short-lived Safe, zero-copy reading
Memory Heap Long-lived / async Buffer management across async ops
ReadOnlyMemory Heap Long-lived / async Read-only buffers
ReadOnlySequence Heap (segmented) Long-lived Streaming or segmented data

Final Thoughts

These memory abstractions bridge the gap between raw performance and managed safety.
They let you write low-level, high-performance code — without stepping outside the managed runtime.

  • Use Span<T> and ReadOnlySpan<T> for short-lived, fast operations.
  • Use Memory<T> and ReadOnlyMemory<T> when you need heap allocation or async compatibility.
  • Use ReadOnlySequence<T> when you’re working with streams or pipelines that handle segmented data.

Understanding these types is key to mastering high-performance C# and modern .NET systems programming.

An error has occurred. This application may no longer respond until reloaded. Reload x