Using C# Span with Arrays for Zero-Allocation Slicing
c# span with arrays: Learn how to use Span<T> with arrays in C# to slice, modify, and pass array data without allocating new memory, and understand the ref struct limi...
When you work with arrays in C#, every slice or copy operation normally allocates a new array. Span<T> changes that by giving you a view over existing array memory without copying it. Using c# span with arrays effectively means understanding when the view is zero-allocation, where it helps, and where its ref struct constraints require a different approach.
Creating a Span from an Array
The simplest way to get a Span<T> from an array is the AsSpan() extension method:
int[] numbers = { 10, 20, 30, 40, 50 }; Span<int> span = numbers.AsSpan();
The span references the same memory as the array. No copy happens. If you modify an element through the span, the array reflects the change immediately:
span[0] = 99; Console.WriteLine(numbers[0]); // 99
This direct memory sharing is the core reason to use spans with arrays. You get array-like indexing and iteration without paying for a second allocation.
Slicing Arrays Without Allocation
The more interesting case is slicing. With a plain array, extracting a subset requires Array.Copy or LINQ's Skip/Take, both of which allocate a new array. With Span<T>, you create a slice that points into the original array:
int[] data = { 1, 2, 3, 4, 5, 6, 7, 8 }; Span<int> middle = data.AsSpan(2, 4); // middle covers { 3, 4, 5, 6 }
The Slice method gives the same result with slightly more explicit syntax:
Span<int> middle = data.AsSpan().Slice(2, 4);
Neither call allocates. The slice is a struct containing a reference to the underlying memory, an offset, and a length. That makes slicing cheap enough to use in hot paths where array copies would create measurable garbage-collection pressure.
Modifying Array Data Through a Span
Because a span is a mutable view, you can write through it to update the underlying array:
Span<int> values = numbers.AsSpan(1, 3); for (int i = 0; i < values.Length; i++) { values[i] *= 2; }
This is useful when you want to operate on a portion of an array without passing the whole array and an index range to every method. A method that accepts Span<int> can work on any contiguous region of array memory, or on stack-allocated memory, without knowing where the data came from:
void Scale(Span<int> target, int factor) { for (int i = 0; i < target.Length; i++) { target[i] *= factor; } }
Call it with an array slice or a stack-allocated buffer:
Scale(numbers.AsSpan(2, 3), 2); Span<int> stackBuffer = stackalloc int[4] { 1, 2, 3, 4 }; Scale(stackBuffer, 3);
The same method serves both cases because Span<T> abstracts over the memory location.
Performance Characteristics
Span<T> is a ref struct, which means it lives on the stack rather than the heap. Creating a span from an array involves no heap allocation, and slicing adds only the cost of copying a few fields (reference, offset, length). The runtime can also optimize bounds-check elimination for span indexing in ways that are harder to guarantee with array indexing, because the span's length is known locally.
That said, spans are not always faster than arrays. If you are iterating an entire array from start to finish, a foreach over the array is already well optimized. The benefit appears when you repeatedly slice, pass sub-ranges to methods, or process data in a loop where avoiding allocations reduces garbage collection frequency. The exact improvement depends on how much allocation pressure you remove, which is workload-specific.
Limitations You Need to Respect
The ref struct nature of Span<T> imposes restrictions that matter when you integrate spans into existing code:
- You cannot store a span in a class field. A class instance lives on the heap, and a span can only reference stack memory or managed array memory, so the compiler forbids the combination.
- You cannot use a span in an
asyncmethod. The state machine that backsasyncmethods stores local variables on the heap, which conflicts withref structsemantics. - You cannot use a span as a generic type argument. Generic instantiations may require boxing or heap storage, which is not allowed for
ref structtypes. - You cannot box a span. Casting it to
objector to an interface is a compile-time error.
These restrictions are not bugs. They exist to guarantee that a span never outlives the memory it references. When you need to pass data asynchronously or store it for later, you must copy the relevant portion into an array first.
Converting a Span Back to an Array
When you need to return data from a span, or store it beyond the current scope, use ToArray():
Span<int> slice = numbers.AsSpan(1, 3); int[] copy = slice.ToArray();
This allocates a new array and copies the span's elements into it. Use it only when necessary, because it defeats the zero-allocation purpose of spans. A common pattern is to keep data in a span during processing and convert once at the boundary where the data leaves the synchronous, stack-based context.
Common Mistakes with Span and Arrays
One frequent mistake is keeping a span after the underlying array has been resized or replaced. If you reassign the array variable, the span still points to the original array:
int[] buffer = new int[10]; Span<int> span = buffer; buffer = new int[20]; // span still references the old 10-element array
The span remains valid and points to the old memory. This is not a safety issue, but it can surprise you when the span does not reflect the new array.
Another mistake is using Length on the span when you meant the original array's length. After slicing, span.Length reports the slice length, not the array length. Code that assumes the span covers the whole array will silently process fewer elements.
A third issue is attempting to use Span<T> in a LINQ query or with methods that expect IEnumerable<T>. Spans do not implement IEnumerable<T> because that would require boxing. You need to convert to an array first, or restructure the code to avoid LINQ.
When to Choose Span Over Array Operations
Use spans when you are slicing arrays frequently, passing sub-ranges to methods, or working in performance-sensitive code where allocations matter. Stick with plain array operations when you need async support, when you need to store the data in a field, or when the code is not on a hot path and the extra abstraction would reduce readability.
The decision is not about replacing arrays. Arrays remain the storage mechanism. Spans are a way to view and manipulate that storage with less overhead and more precise control over what portion of the data you touch.