Span and Memory in C# 7.2+

Span and Memory in C# 7.2 and later

Why care about these types?

Span<T> (introduced in C# 7.2) and its sibling Memory<T> let you work with contiguous blocks of data without extra allocations or copies—all while staying within C#’s safety rules. Think of them as zero-cost, slice-style views over memory that can point to the stack, the managed heap, or even unmanaged buffers.

Typical use cases include:


Span<T> at a glance

Highlight What it means
Value type (ref struct) Lives on the stack; passing it around is almost free.
Stack-only Cannot escape the current method, so the JIT knows exactly when it dies.
Universal view The same API slices arrays, stackalloc blocks, and unmanaged memory.

A minimal example

int[] data = { 1, 2, 3, 4, 5 };

Span<int> span   = data;        // entire array
Span<int> middle = span[1..4];  // elements 1-3

middle[0] = 99;                 // mutates data[1]
Console.WriteLine(string.Join(", ", data));
// 1, 99, 3, 4, 5

The slice is just a lightweight view; no element has been copied.

Using stackalloc

Span<byte> buffer = stackalloc byte[256];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = (byte)i;

You get 256 bytes on the stack—zero GC pressure, automatic cleanup when the method returns.


Enter Memory<T>

Span<T> cannot survive beyond the current stack frame or cross an await. When you need that flexibility, switch to Memory<T> (or its read-only counterpart ReadOnlyMemory<T>).

Aspect Span<T> Memory<T>
Stored on Stack only Heap or stack
Works across await / iterators
Mutability Read/write Read/write (ReadOnlyMemory<T> is read-only)

Async-friendly example

public async Task ParseAsync(ReadOnlyMemory<byte> packet)
{
    var header = packet[..4];
    var body   = packet[4..];

    await Task.Yield();          // safe: Memory survives across awaits

    Console.WriteLine(
        $"Header={BitConverter.ToUInt32(header.Span)} Body bytes={body.Length}");
}

Inside synchronous code blocks you can still call .Span to get the fast, stack-only view.


Practical tips


Caveats and gotchas

  1. Escape rules The compiler forbids passing a Span<T> to code that might outlive the current method. Respect those errors—they prevent use-after-free bugs.
  2. Heap fragmentation Storing many small Memory<T> instances as long-lived fields can fragment the LOH. Pool or reuse where possible.
  3. Platform support Requires .NET Core 2.1+, .NET 5+, or .NET Standard 2.1. Older Unity or Xamarin versions may lack full support.
  4. Slicing unmanaged data MemoryMarshal.CreateSpan lets you wrap raw pointers, but handle lifetime manually and beware of pinning costs.