Span<T> and Memory<T>

C# · Advanced · 14 min

Spans are how high-performance .NET code (parsers, ASP.NET Core, serializers) avoids allocations.

Span<T> is a view over a contiguous region of memory: part of an array, a string (as ReadOnlySpan<char>), stack memory from stackalloc, or native memory. Slicing a span (span[2..5] or Slice) creates a new view with no copying and no allocation.

ReadOnlySpan<char> s = "key=value";
var key = s[..s.IndexOf('=')];   // no substring allocation

Many APIs accept spans: int.Parse(ReadOnlySpan<char>), MemoryExtensions methods like Split, Trim, SequenceEqual, and Stream.Read(Span<byte>). C# 14 adds implicit conversions so arrays pass to span parameters more naturally.

Span<T> is a ref struct: it can only live on the stack. It can't be a field of a class, be boxed, be captured by lambdas, or (before C# 13) be used across await or in iterators. These rules guarantee it never outlives the memory it points to.

When you need to store a slice on the heap or keep it across awaits, use Memory<T> / ReadOnlyMemory<T> and get a span from .Span when you process it.

Related tools:

  • stackalloc byte[256] gives a small stack buffer assignable to Span<byte>.
  • ArrayPool<T>.Shared.Rent/Return reuses large buffers.
  • string.Create and ISpanFormattable write into buffers directly.

Measure first: spans pay off in hot paths and libraries, not ordinary business code.

Example

static int SumCsv(ReadOnlySpan<char> line)
{
    int total = 0;
    foreach (var range in line.Split(','))       // no string allocations
        total += int.Parse(line[range].Trim());
    return total;
}
Console.WriteLine(SumCsv("1, 2, 3,40"));          // 46

int[] data = [1, 2, 3, 4, 5, 6];
Span<int> middle = data.AsSpan(2, 3);             // view over 3,4,5
middle[0] = 99;
Console.WriteLine(string.Join(",", data));        // 1,2,99,4,5,6

Span<byte> buf = stackalloc byte[4];              // stack memory
buf.Fill(0xAB);
Console.WriteLine(Convert.ToHexString(buf));      // ABABABAB

ReadOnlyMemory<char> mem = "hello world".AsMemory(6);
await Task.Yield();                               // Memory survives awaits
Console.WriteLine(mem.Span.ToString());           // world

Remember

  • Span<T> slices memory without copying
  • It is a ref struct: stack-only, no fields in classes or boxing
  • Use Memory<T> to store slices on the heap or across awaits
  • Span-based APIs avoid substring and array allocations

Watch out for

  • Rewriting ordinary code with spans before profiling shows an allocation problem.

Check yourself

  1. Why can't Span<T> be a field of a class?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/