Using ReadOnlySpan in C# for Slice and Validation
Learn how c# readonlyspan enables safe, allocation-free slicing and validation of contiguous data, with practical examples and performance insights.
When you need to parse a substring, validate a byte sequence, or process a slice of a buffer without creating a new array or string, c# readonlyspan provides a way to work directly with the underlying memory. ReadOnlySpan<T> is a ref struct that represents a contiguous region of memory that you can only read. It is not allocated on the heap, which makes it suitable for performance-sensitive paths where avoiding allocations matters.
The most common use is to avoid string allocations during text processing. Instead of calling Substring and paying for a new string, you can create a ReadOnlySpan<char> that points into the original string and operate on that. This is especially relevant in parsers, protocol handlers, and logging pipelines where the same buffer is processed repeatedly.
Creating a ReadOnlySpan Over a String
A string implicitly converts to ReadOnlySpan<char>. You can also create a span over an array, an array segment, or memory from a Memory<T>. The simplest way to start is with a string literal or a string variable.
string message = "order:12345:paid"; ReadOnlySpan<char> span = message;
The conversion is implicit and does not copy the string's data. The span points directly at the first character of the string. You can then use the span's Slice method to get a subset without allocating a new string.
int firstColon = span.IndexOf(':'); ReadOnlySpan<char> idPart = span.Slice(0, firstColon);
This creates a new span that references the same underlying memory as the original string. No characters are copied. The Slice method returns a ReadOnlySpan<char> that starts at the specified index and has the specified length.
Reading and Validating Data Without Allocation
Once you have a ReadOnlySpan<char>, you can enumerate it, compare it, or pass it to methods that accept ReadOnlySpan<char>. The .NET base class library includes many methods that accept spans, such as int.Parse overloads, Guid.Parse, and IPAddress.TryParse. This lets you parse a specific segment of a string without creating a substring.
public bool TryParseOrderId(string line, out int orderId) { ReadOnlySpan<char> span = line; int colonIndex = span.IndexOf(':'); if (colonIndex < 0) { orderId = 0; return false; } ReadOnlySpan<char> idSpan = span.Slice(0, colonIndex); return int.TryParse(idSpan, out orderId); }
int.TryParse has an overload that accepts ReadOnlySpan<char>. The same applies to other numeric types and many parsing APIs. This pattern eliminates the Substring allocation that would otherwise occur if you converted the slice to a string first.
Slicing Without Copying: How It Works
ReadOnlySpan is a ref struct that stores a reference to the start of the region and the length. The Slice method returns a new ref struct with an adjusted reference and length. There is no object allocation, and the operation completes in constant time.
ReadOnlySpan<char> full = "abcdef".AsSpan(); ReadOnlySpan<char> middle = full.Slice(2, 2); // "cd"
The runtime stores a managed pointer to the first element of the original data. When you slice, you simply move that pointer and adjust the length. This is why slicing a span is cheap and does not put pressure on the garbage collector.
Practical Scenario: Parsing Key-Value Pairs
Consider a simple key-value parser for a configuration line. Without spans, you might write code that creates multiple substring allocations. With c# readonlyspan, you can process the input using only stack-allocated structs and no heap allocations.
public static void ParseKeyValue(ReadOnlySpan<char> line) { int equalsIndex = line.IndexOf('='); if (equalsIndex < 0) return; ReadOnlySpan<char> key = line.Slice(0, equalsIndex); ReadOnlySpan<char> value = line.Slice(equalsIndex + 1); Console.WriteLine($"Key: {key.ToString()} Value: {value.ToString()}"); }
The ToString() calls in the example produce strings, but the span operations themselves do not allocate. If you need to log the values, the conversion is necessary. However, if you only need to compare or transform the value, you can avoid converting to a string entirely.
Performance Considerations in ReadOnlySpan
The main performance advantage of ReadOnlySpan is the reduction in heap allocations. Creating a substring or an array slice forces the runtime to allocate new memory and copy the elements. A span slice does not allocate and does not copy. This reduces garbage collection pressure, which can improve throughput in high-frequency code paths.
There is a more subtle benefit: ReadOnlySpan can be used with stack allocated memory via stackalloc. Because a ref struct cannot be boxed, it can safely reference stack memory. This enables patterns that would be risky with arrays because arrays always live on the heap.
Span<byte> buffer = stackalloc byte[256]; ReadOnlySpan<byte> data = buffer; // populate data and process it without additional allocations
This is valuable when handling network packets or file buffers where you want to avoid allocating a byte array for every operation. The stack is the right place for short-lived, small-sized buffers.
When ReadOnlySpan Is Not the Right Choice
The restrictions around ref structs mean you cannot use ReadOnlySpan<T> as a field in a class or as a generic type argument. You also cannot use it in async methods because the compiler cannot guarantee that the span's memory remains valid across an await boundary. If you need to store the data for later consumption or pass it to an async method, you must materialize it into a string or array first.
public async Task ProcessAsync(ReadOnlySpan<char> span) // compile error { // await is not allowed with span in scope await Task.Delay(10); }
The compiler error is intentional: the memory that the span points to might be invalid once the method yields. The same restriction applies to iterators and to capturing a span in a lambda that outlives the current stack frame.
Stackalloc and Reference Restrictions
Because ReadOnlySpan<T> is a ref struct, the runtime can enforce that the memory it points to is valid for the duration of the span's existence. This makes stackalloc safe to use with spans: the memory lives on the stack, and the span cannot escape the stack frame. The compiler ensures you cannot store a span in a class field or return it to a caller that would outlive the stack frame.
public ReadOnlySpan<byte> CreateSpan() { Span<byte> buffer = stackalloc byte[10]; return buffer; // returns a span pointing to a stack location }
This code compiles, but the returned span is valid only until the method returns. Using it after that point is undefined behavior, and the runtime may detect the misuse or may silently produce garbage. In practice, you should only use stackalloc for spans that stay within one method.
Compatibility and .NET Versions
ReadOnlySpan<T> was introduced in .NET Core 2.1 and .NET Standard 2.1. If you are targeting .NET Framework, you need the System.Memory NuGet package to use it. When the package is installed, Span<T> and ReadOnlySpan<T> are available, but some APIs that accept spans may not exist in the .NET Framework base libraries. For example, int.TryParse(ReadOnlySpan<char>) is part of the modern .NET runtime, but an older framework might not have that overload. In that case, you would need to convert the span to a string first, which defeats the purpose.
When targeting modern .NET (Core 3.0 or later), the majority of string and numeric parsing APIs have span overloads. The trend in the BCL is to add span-based methods to reduce allocations. Before using a particular method with a span, verify that the target framework includes the overload.
Decision Criteria for Using ReadOnlySpan in C#
Choose ReadOnlySpan<T> when you have a hot path that processes many small slices of the same buffer or string. Typical cases include parsers, serialization, and binary protocol decoding. If the data is processed once and then discarded, spans offer significant savings.
If the data needs to be stored, sent to another thread, or included in an async operation, materialize it into a string or array. There is no point fighting the compiler restrictions; the cost of one string allocation is often acceptable outside the hottest path.
For line-by-line processing of small strings, a manual loop using ReadOnlySpan<char> can outperform Split because Split creates an array of substrings. However, code that avoids Split is more complex. Use that approach when profiling shows that allocation pressure is the bottleneck. In general, reach for ReadOnlySpan when you need to extract multiple values from a single buffer and you want to minimize allocations.
Advanced Slicing with Multiple Delimiters
You can combine Slice with methods like IndexOfAny to split on multiple separators. This is useful for parsing CSV-like input without allocating substrings.
ReadOnlySpan<char> line = "name;age;city"; int current = 0; while (true) { int nextSemicolon = line.Slice(current).IndexOf(';'); int length = nextSemicolon < 0 ? line.Length - current : nextSemicolon; ReadOnlySpan<char> field = line.Slice(current, length); ProcessField(field); if (nextSemicolon < 0) break; current += nextSemicolon + 1; }
This loop processes each field without creating a single string. The ProcessField method can accept a ReadOnlySpan<char> and handle the field directly. This pattern is common in high-throughput log parsers or telemetry pipelines.