Span<T> is a stack-only type that represents a contiguous region of memory — a slice of an array, a stack-allocated buffer, or native memory — without copying. It is the fundamental primitive for high-performance, zero-allocation data processing in .NET. Where previously you might call string.Substring() to extract a portion of text (allocating a new string), ReadOnlySpan<char> lets you represent and work with that slice without any allocation. ASP.NET Core’s HTTP parser, JSON parser, and routing engine are built on spans, which is a significant reason for their throughput.
Span Fundamentals
// ── Creating spans from arrays ─────────────────────────────────────────────
int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Span<int> full = array; // implicit conversion
Span<int> slice = array.AsSpan(2, 5); // elements [2..7) = {3,4,5,6,7}
ReadOnlySpan<int> ro = array.AsSpan(0, 3); // read-only view: {1,2,3}
// Modify through span — modifies the original array
slice[0] = 99;
Console.WriteLine(array[2]); // 99 — span and array share the same memory
// ── String spans — zero-allocation slicing ────────────────────────────────
string csv = "alice@example.com,bob@test.org,carol@web.net";
ReadOnlySpan<char> span = csv.AsSpan();
int comma = span.IndexOf(',');
ReadOnlySpan<char> first = span[..comma]; // "alice@example.com" — no allocation!
ReadOnlySpan<char> rest = span[(comma + 1)..]; // "bob@test.org,carol@web.net"
// Compare without allocating
bool isAlice = first.Equals("alice@example.com", StringComparison.Ordinal);
// Convert to string only when you actually need a string (at boundary)
string firstStr = first.ToString(); // one allocation at the boundary
Span<T> is a ref struct — it can only exist on the stack. This means you cannot store a Span<T> in a class field, in a generic type parameter, in an array, or use it across an await boundary. The stack-only constraint is enforced by the compiler. When you need a slice that survives beyond the current method (e.g., stored in a field or passed across async boundaries), use Memory<T> instead — it is a heap-compatible equivalent that wraps the same underlying data.string (e.g., as a dictionary key or to return to a caller). This pattern can reduce allocations by 90%+ in parsing-heavy code paths. ASP.NET Core’s HTTP/2 header parsing uses exactly this approach.Span<T> in async methods — the compiler will reject it. The restriction is: if a method uses await, it becomes a state machine stored on the heap, and Span<T> cannot be stored on the heap. The solution is to separate the span-based synchronous work into a non-async method, then call that method from the async method. The async method receives the result (a value type or a string), while the sync method does all the span manipulation.Memory<T> — Heap-Compatible Slice
// Memory<T> — can be stored in fields, arrays, passed across async
public class BufferedReader
{
private readonly Memory<byte> _buffer; // stored as a field — impossible with Span
public BufferedReader(byte[] data)
=> _buffer = data.AsMemory(); // Memory wraps the same array, no copy
public async Task ProcessAsync(CancellationToken ct)
{
// Get a span from Memory for the actual work (inside sync block)
ReadOnlySpan<byte> span = _buffer.Span; // ← Span only here, not stored
// Can await inside an async method because we're using Memory, not Span
await SaveResultAsync(ParseHeader(span), ct);
}
private string ParseHeader(ReadOnlySpan<byte> span)
{
// Span-based parsing — synchronous, no await, no heap allocation
return System.Text.Encoding.UTF8.GetString(span[..16]);
}
}
// ── Memory.Slice ───────────────────────────────────────────────────────────
byte[] data = new byte[1024];
Memory<byte> mem = data.AsMemory();
Memory<byte> first = mem.Slice(0, 512); // first 512 bytes
Memory<byte> last = mem[512..]; // last 512 bytes
// Convert to Span when doing the actual processing
ProcessData(first.Span); // Span used only in this synchronous call
Parsing Without Allocation
// Parse CSV line into fields without allocating strings for each field
public static List<string> ParseCsvLine(string line)
{
var fields = new List<string>();
var span = line.AsSpan();
while (!span.IsEmpty)
{
int comma = span.IndexOf(',');
if (comma == -1)
{
fields.Add(span.Trim().ToString()); // last field
break;
}
fields.Add(span[..comma].Trim().ToString()); // allocate only at boundary
span = span[(comma + 1)..]; // advance without allocation
}
return fields;
}
Common Mistakes
Mistake 1 — Storing Span in a class field (compile error)
❌ Wrong — compile error: cannot store ref struct in a heap type:
public class Parser { private Span<byte> _buffer; } // compile error!
✅ Correct — store Memory<byte> in the field; get Span<byte> when needed:
public class Parser { private Memory<byte> _buffer; }
Mistake 2 — Using Span across an await boundary (compile error)
❌ Wrong — compile error inside an async method:
async Task ProcessAsync()
{
Span<byte> span = buffer.AsSpan();
await SomeWorkAsync(); // compile error — Span cannot survive across await
}
✅ Correct — extract span work into a synchronous helper method.