Slice Strings Without Allocating: ReadOnlySpan<T> in C#
Use ReadOnlySpan<char> to slice strings without heap allocations. Learn ref struct limits and a parsing example.
17 Aug 2025, 20:58 UTC

The problem: parsing creates garbage
You have a hot path that reads comma-separated lines from a log file. The natural approach is line.Substring(start, length) or line.Split(','). Every call allocates a new string on the managed heap. If you process millions of lines, the garbage collector spends more time reclaiming short-lived strings than running your code.
The useful takeaway: ReadOnlySpan<char> lets you slice into an existing string without copying it. You get a view, not a new object.
What ReadOnlySpan<T> actually is
A span is a type-safe reference to a contiguous region of memory. It can point at an array, a string, or stack-allocated memory. The key constraint is that ReadOnlySpan<T> is a ref struct: it lives on the stack, cannot be boxed, cannot be a generic type argument, and cannot be used as a field in a class.
Because it is stack-only, you cannot store a span in a class instance or capture it across an await boundary. It is designed for synchronous, short-lived work inside a single method.
Worked example: parse one field without copying
Suppose each log line looks like 2026-10-18,ERROR,Something failed. You want the severity level, the second field.
using System;
ReadOnlySpan<char> line = "2026-10-18,ERROR,Something failed".AsSpan();
int firstComma = line.IndexOf(',');
ReadOnlySpan<char> rest = line.Slice(firstComma + 1);
int secondComma = rest.IndexOf(',');
ReadOnlySpan<char> severity = rest.Slice(0, secondComma);
Console.WriteLine(severity.ToString()); // ERROR
AsSpan() comes from MemoryExtensions and creates a view over the string's existing characters. Slice adjusts the view's start and length without allocating. Only the final ToString() creates a new string, and you can skip it if the next API accepts a span.
The allocation-heavy equivalent would call Substring twice, once for each field, producing two new strings per line.
Where spans break down
- Ref struct rules: You cannot declare
ReadOnlySpan<T>as a class or struct field, use it as a generic type argument, or box it. - Async methods: You cannot use a span across an
awaitpoint because the compiler cannot guarantee the stack location survives a suspension. - Unsafe memory: Creating a span from an
unsafepointer is valid, but if the underlying memory is freed while the span exists, you have memory corruption.
Check it yourself
Create a console application in a terminal with no special permissions:
dotnet new console -n SpanDemo
cd SpanDemo
dotnet run
Replace Program.cs with a loop that parses one million lines using Substring, then repeat using ReadOnlySpan<char>. Run each version under a .NET memory profiler or BenchmarkDotnet with [MemoryDiagnoser]. Look for fewer System.String allocations and fewer Gen 0 collections in the span version.
Expected check: the span version should show zero per-line string allocations for the slicing work itself. Risk: microbenchmarks can be misleading if the JIT eliminates dead code, so keep the results used by the program.
When to use it
Use ReadOnlySpan<T> in hot paths such as log parsers, binary protocol readers, or serializers where slicing happens millions of times. Do not replace every string method; spans add complexity and cannot cross await boundaries. Start by swapping Substring for AsSpan and Slice inside tight loops, then measure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.