Back to Blog
C#

Slicing Spans in C#

Learn how to use Span<T>.Slice to create memory views without allocations, with practical examples and performance considerations.

Span<T>C# performanceMemory managementSlicingReadOnlySpan<T>Stackalloc
Illustration of a C# Span slicing a byte array into segments without copying memory.

To create a view into a contiguous region of memory without copying that memory, Span<T> and ReadOnlySpan<T> expose the Slice method. Understanding how Slice works—and when to use it—can help you write code that avoids unnecessary allocations in hot paths, such as parsers, network protocols, or binary file readers.

The Core Slice Method

Both Span<T> and ReadOnlySpan<T> expose an instance method named Slice. The method has two overloads:

public Span<T> Slice(int start); public Span<T> Slice(int start, int length);

For ReadOnlySpan<T>, the same overloads return ReadOnlySpan<T>.

The first overload returns a span that starts at index start and extends to the end of the original span. The second returns a span that starts at start and has the specified length. The operation is O(1); it does not copy elements. It only adjusts the reference and length tracked by the span value.

Because a span is a ref struct, a local span value is not allocated on the managed heap. This is the key to Slice's performance benefit in allocation-sensitive code.

Slicing Without Copying

Consider a common scenario: you have a byte array that holds a fixed header followed by a payload. Instead of copying the payload into a new array, you can slice the original buffer to create a window over it.

byte[] buffer = GetNetworkPacket(); // assume this returns some bytes Span<byte> packet = buffer; Span<byte> header = packet.Slice(0, 8); // first 8 bytes Span<byte> payload = packet.Slice(8); // everything after the header

Here, header and payload are views over the same underlying buffer. No new array is allocated. If you change an element through payload, the change is visible in buffer as well, because they share the same backing memory. This behavior is different from ArraySegment<T>, which also references a segment of an array, but spans offer a more general and safer abstraction, especially when combined with stack memory or native memory.

Practical Example: Parsing a Binary Format

Suppose you are parsing a binary format where each record starts with a 4-byte length field followed by that many data bytes. Without slicing, you might copy each record into a new array. With Span<T>, you can walk through the buffer without allocations.

static void ParseRecords(ReadOnlySpan<byte> data) { int offset = 0; while (offset < data.Length) { int recordLength = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(offset, 4)); offset += 4; ReadOnlySpan<byte> record = data.Slice(offset, recordLength); offset += recordLength; ProcessRecord(record); } } static void ProcessRecord(ReadOnlySpan<byte> record) { // Interpret the record's bytes without copying them. }

In this example, BinaryPrimitives.ReadInt32LittleEndian reads an integer from the span without copying, and record is a view of the original data. The slicing itself uses zero heap allocations, which is useful when parsing many records in a tight loop.

ReadOnlySpan<T> vs Span<T>

When you only need to read from a slice, use ReadOnlySpan<T> as the parameter type. It prevents accidental modification and communicates intent clearly. ReadOnlySpan<T>.Slice returns a ReadOnlySpan<T>. If you need to modify the content, use Span<T> and its Slice method.

Choosing the right type also affects performance indirectly. ReadOnlySpan<T> can be created from string via AsSpan(), allowing you to slice strings without allocations.

string input = "123,456,789"; ReadOnlySpan<char> span = input.AsSpan(); int firstComma = span.IndexOf(','); ReadOnlySpan<char> firstNumber = span.Slice(0, firstComma); ReadOnlySpan<char> rest = span.Slice(firstComma + 1);

This pattern is common in CSV parsers and configuration file parsers where you need to extract substrings without allocating new strings. Slice on a ReadOnlySpan<char> returns a new ReadOnlySpan<char> that references the original string's characters.

Common Mistakes and Edge Cases

One common mistake is passing invalid indices to Slice: a negative start or length, a start greater than the span length, or a length that exceeds the available space. These cases throw an ArgumentOutOfRangeException. The exception is thrown even in release builds because spans are designed to be safety-checked by default. The simplest protection is to validate your indices before calling Slice.

Another subtlety is that Slice returns a new span instance with its own start and length. It does not mutate the original span, so if you need to use the sliced view, assign it to a variable.

Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 }; Span<int> slice = numbers.Slice(1, 3); // slice is [2, 3, 4] slice[0] = 20; // numbers[1] is now 20

The stackalloc usage above is another area where spans shine: they can reference memory on the stack. Slicing stack memory is just as efficient as slicing an array.

Performance Considerations

Slicing a span is O(1) and does not allocate, but the performance benefit is only realized if you avoid converting the span back to an array or to a string, because those operations allocate and copy. For example, returning slice.ToArray() at the end of a method defeats the purpose of slicing. Instead, keep the operation within the span realm for as long as possible.

When you need to pass a sliced span to a method, that method must accept ReadOnlySpan<T> or Span<T> as a parameter. This can propagate through your codebase, but that is often acceptable for internal methods that process large data.

public static void WriteToStream(Stream stream, ReadOnlySpan<byte> data) { stream.Write(data); }

Here, stream.Write is the ReadOnlySpan<byte> overload available in .NET Core 2.1+ and .NET Standard 2.1+, so you can pass the slice directly without copying to a byte[].

Compatibility Constraints

Span<T> is available in .NET Core 2.1+, .NET Standard 2.1+, and .NET 5+. The System.Memory package provides it for earlier platforms, including .NET Framework, but many older framework APIs do not accept spans, so converting to arrays may be necessary.

Slicing a string's underlying characters is possible with ReadOnlySpan<char> through AsSpan(), but you must be careful not to capture the span in a lambda or async method, because ref structs cannot be used across await boundaries. For that, you would need to copy the relevant data into a heap object first.

Modifying Data Through Slices

Because a slice shares memory with the original, any modification through the slice is visible in the original. This can be useful for in-place transformation, but it can also cause subtle bugs if you assume you are working on a copy.

static void UppercaseFirst(ReadOnlySpan<char> text) { // ReadOnlySpan cannot modify, so this is not possible. } static void UppercaseFirst(Span<char> text) { if (text.Length > 0) { char c = text[0]; text[0] = char.ToUpperInvariant(c); } }

Span<char> is for modifying a mutable character buffer, such as a char[] or native memory. Strings are immutable by design, so you should not try to mutate a string through a span. If you need in-place character changes, use a char[] or another mutable buffer.

When to Use Slice vs Memory<T>

Memory<T> can be stored in fields, used in async methods, and passed to APIs that cannot accept spans. Memory<T>.Slice works similarly, but it returns another Memory<T>. You can then call .Span on a Memory<T> to obtain a span.

Use Memory<T> when you need to store a slice beyond the current method and use Span<T> when the slice is used only within a synchronous, stack-bound context. Generally, prefer spans for fast, allocation-free processing, and fall back to Memory<T> when asynchrony or storage requires it.

Memory<char> memory = new char[8]; Memory<char> slice = memory.Slice(1, 3); Span<char> spanSlice = slice.Span;

This shows how a Memory<T> slice can be converted to a span when needed. The conversion is O(1) and does not allocate.

Slicing Arrays with AsSpan

To slice an array without copying, use the MemoryExtensions.AsSpan extension. MemoryExtensions provides AsSpan and AsMemory overloads that create views over existing arrays. For example:

int[] numbers = { 10, 20, 30, 40 }; Span<int> subset = numbers.AsSpan(1, 2); // {20, 30}

This AsSpan(int start, int length) overload returns a view over the original array without allocation. If you actually need a sub-array copy, allocate a new array and copy the range; that allocation is exactly what span-based slicing avoids.

Ref Struct Constraints and Async

Since Span<T> is a ref struct, you cannot use it in async methods, iterators, or lambdas that capture it. This means you often have to separate synchronous processing from async I/O. For asynchronous parsing, use Memory<T> instead. The rule is simple: if you need to await while holding the data, use Memory<T>. Otherwise, Span<T> is fine.

Key Takeaway on Slicing

Slice is the primary way to create a window over existing memory in C#. It is a zero-copy operation that works on arrays, strings, and stack-allocated memory. The main tradeoff is that spans cannot be boxed or stored in heap-allocated fields, and they cannot be used across await, so their usefulness is limited to synchronous code paths. Nevertheless, for high-performance data processing, Slice is an indispensable technique.

Slicing Spans in C#: Zero-Copy Memory Views | RYUSLOG DEV