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:

  • High-frequency text or binary parsing where every allocation hurts
  • Large streaming buffers (files, sockets, images) that you need to carve into smaller pieces
  • Performance-critical code paths where GC pauses must be kept to a minimum

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

  • Favor ReadOnlySpan<T> for input parameters to make intent crystal-clear and prevent accidental writes.
  • Chain slices instead of copying: every slice ([..] or Slice) is O(1).
  • Span<T> pairs naturally with TryParse / TryFormat APIs for allocation-free conversions.
  • When you must store data or keep it after an await, promote to Memory<T> early and convert back to .Span only for short, synchronous work.

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.