Span
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. |
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.
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.
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) |
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.
ReadOnlySpan<T> for input parameters to make intent crystal-clear and prevent accidental writes.[..] or Slice) is O(1).Span<T> pairs naturally with TryParse / TryFormat APIs for allocation-free conversions.await, promote to Memory<T> early and convert back to .Span only for short, synchronous work.Span<T> to code that might outlive the current method. Respect those errors—they prevent use-after-free bugs.Memory<T> instances as long-lived fields can fragment the LOH. Pool or reuse where possible.MemoryMarshal.CreateSpan lets you wrap raw pointers, but handle lifetime manually and beware of pinning costs.