GAP·MAP

C# Span<T> and memory

[ middle depth ]

What Span<T> is and why it never allocates

A Span<T> is a struct with two fields: an internal managed reference and an int length. Creating one, or slicing one, copies those two fields and nothing else, so there is no heap allocation and no copy of the underlying elements. It is a typed window over memory that someone else owns: a managed array, a stackalloc block, a string (as ReadOnlySpan<char>), or native memory.

byte[] data = new byte[1000];
Span<byte> body = data.AsSpan(100, 50);   // no allocation, a view over data
body[0] = 42;                              // writes through: data[100] == 42

ReadOnlySpan<T> is the read-only form. A string gives you one through AsSpan(), since strings are immutable and cannot hand out a writable view.

Slicing aliases the backing memory

Because a span holds a reference into the original storage, writing through a slice mutates the original. Slice(start, length) and the range indexer span[2..7] both return another view over the same bytes. This is what makes span-based parsing cheap: int.Parse(line.AsSpan(16)) reads the number straight out of the string with no Substring, which would allocate a new string and copy characters. Reach for ToArray() only when you need a detached copy, because it allocates and copies.

stackalloc for short stack buffers

stackalloc carves a small block out of the current stack frame. Assigned to a span you stay in safe code, no unsafe context required:

Span<byte> buffer = stackalloc byte[256];

The block is freed automatically when the method returns, and it is not subject to garbage collection. Two facts trip people up. The memory is not zeroed, so its contents are undefined until you fill it or call buffer.Clear(). And the stack is small, so a large or unbounded size throws a StackOverflowException that cannot be caught. Keep the size a bounded constant (libraries usually cap around 128 to 1024 bytes) and fall back to a heap array above that.

Why you cannot store a Span, and what Memory<T> is for

A span cannot be a field of a class, cross an await, or be captured by a lambda. It is stack-only by design, so it cannot ride along on a heap object. When you need to hold a buffer on the heap or carry it through async code, use Memory<T> or ReadOnlyMemory<T>, which are ordinary structs. Hold the Memory<T> across the await, then read its .Span property at the synchronous point where you touch the bytes.

async Task ProcessAsync(Memory<byte> buffer, Stream s)
{
    int n = await s.ReadAsync(buffer);      // Memory survives the await
    Parse(buffer.Span.Slice(0, n));         // get a Span only for the sync work
}
[ senior depth ]

Why the ref struct rules exist

A ref struct lives on the stack and the compiler guarantees it never escapes to the heap. That guarantee produces the whole restriction list: a ref struct cannot be an array element, a field of a class or a non-ref struct, boxed to object or an interface, captured by a lambda or local function, or held across an await or a yield. Each forbidden case is the same case underneath. An array, a class instance, a boxed value, a closure, and an async or iterator state machine all live on the heap, and hoisting a stack-only value into any of them would let its reference outlive the frame it points at.

The async rule shifted in C# 13. Before it, a ref struct local was banned from async methods outright; since C# 13 the ban is narrowed to the block that contains the await, so a synchronous helper or a pre-await scope is fine. Iterators loosened the same way: ref structs are allowed except in segments with a yield return. C# 13 also lets a ref struct satisfy a type parameter marked allows ref struct and implement interfaces, though converting to the interface still boxes and is rejected.

stackalloc failure modes and the escape rule

Wrong "stackalloc is always the fast, safe choice for a scratch buffer." It is safe only when bounded. An input-driven size, or a stackalloc inside a loop, grows the frame until the stack is exhausted, and the resulting StackOverflowException cannot be caught and terminates the process. Hoist the buffer out of loops, cap the size with a constant, and fall back to ArrayPool or new[] above the cap.

const int Max = 512;
Span<byte> buf = len <= Max ? stackalloc byte[Max] : new byte[len];

You also cannot return a span backed by stackalloc or any local. Ref-safety analysis rejects it because the frame dies on return and the reference would dangle. A span over a heap array or a caller-supplied buffer returns fine.

The ArrayPool rent-and-return contract

ArrayPool<T>.Shared.Rent(n) hands back an array of at least n elements, often larger, with contents left over from the previous renter. So array.Length is not your data length: track the logical count yourself and slice array.AsSpan(0, n). Return exactly once, in a finally, then drop the reference.

byte[] rented = ArrayPool<byte>.Shared.Rent(needed);
try { Work(rented.AsSpan(0, needed)); }
finally { ArrayPool<byte>.Shared.Return(rented); }

Return takes a clearArray flag, default false; pass true when the buffer held secrets, since the next renter can otherwise read them. Using a buffer after returning it, or returning it twice, is a documented use-after-free and double-free bug, not a style nit.

Span equality and the one-way road to Memory

Wrong "span1 == span2 tells me the contents match." On spans == compares reference and length identity, and Span.Equals(object) and GetHashCode are obsolete and throw NotSupportedException. Use the SequenceEqual extension for content equality. The Memory<T> bridge is one-directional too: memory.Span gives you a span at the point of synchronous use, but there is no span-to-Memory conversion, because a stack-only value cannot be recovered into a heap-capable one. The working split is to keep Memory<T> for storage and async and take its .Span only inside the synchronous window.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more C# / .NET

was this useful?