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
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>andReadOnlySpan<T>for short-lived, fast operations. - Use
Memory<T>andReadOnlyMemory<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.


