Updated on
Array slicing is how we take a contiguous run of elements out of an array, and the result is what most of us call a subarray. Since C# 8 one expression does it: numbers[1..4] returns the elements at indices 1, 2 and 3.
That is not the only way, and the others are not interchangeable. Some produce a new array, some wrap the array we already have without copying anything, and the difference decides whether a later change to the original shows up in our slice.
Let’s start.
What Is Array Slicing in C#?
Array slicing is taking a contiguous run of elements out of an array and working with just that run. The result is commonly called a subarray, and in C# one expression produces it: numbers[1..4].
Almost every technique here is one of two things: a copy or a view.
A copy puts the elements into a second, separate array. Array.Copy() does that, and so does the range operator when we apply it to an array. Writing to the copy leaves the original untouched.
A view allocates nothing. ArraySegment<T>, Span<T> and ReadOnlySpan<T> each wrap the array we already have and remember where the slice starts and how long it is, so reading through the view reads the original memory. Writing through a writable view writes to the original array.
Copies are safe and cost memory. Views cost nothing and share state, so a change made through one is visible everywhere that array is used. Whether the slice must stay independent decides which one we want.
Now, let’s jump into code to see how to slice arrays in C# in action.
How Do We Get a Subarray With the Range Operator in C#?
The range operator gives us a subarray in one expression. numbers[1..4] returns the elements at indices 1, 2 and 3, because the start index is included and the end index is not.
Either operand can be left out. numbers[2..] runs from index 2 to the end, numbers[..2] runs from the start to index 1, and numbers[..] is the whole array.
The ^ operator counts from the end, so numbers[^1] is the last element and numbers[^2..] is the last two.
Ranges do not run backwards. numbers[3..1] throws an ArgumentOutOfRangeException at run time rather than reversing the elements.
On an array, the range operator allocates. The compiler turns numbers[1..4] into a call to RuntimeHelpers.GetSubArray(), which hands back a new array holding copies of those elements, so writing to the result never touches the original.
On a Span<T> the same syntax allocates nothing, because the result is another span over the same memory.
Let’s see these forms on a small array:
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..]; // From index 2 to the end
var slice2 = array[..2]; // From the start to index 1
var slice3 = array[1..3]; // From the index 1 to index 2
var slice4 = array[..]; // The whole array
var slice5 = array[3..1]; // Throws ArgumentOutOfRangeException
As we see, we can use either, both, or no operands. We can also see that these boundaries don’t work in the backward direction, so slice5 throws an ArgumentOutOfRangeException.
One thing worth mentioning is if we use the range operator to work with arrays, it allocates new arrays. But it is not the case when we use it on a Span.
The syntax has more to it than slicing, and we cover ranges and indices, including the index from end operator, in a separate article.
How Do We Slice an Array With LINQ Skip and Take?
Let’s say we have an array of ten posts and we only want to return five of them to the user, with the intention to return the rest of them on demand. This is known as pagination and is achieved using two LINQ methods Skip() and Take(). So let’s import LINQ and consume these two methods:
var posts = new string[] { "post1", "post2", "post3", "post4", "post5", "post6", "post7", "post8",
"post9", "post10" };
var slicedPosts = posts.Skip(0).Take(5);
foreach (var post in slicedPosts)
Console.WriteLine(post); // Outputs the first 5 posts
We pass the number of elements we want to skip to the Skip() method. This turns out to be the starting index, because if we’re starting at index 0 then we’re skipping 0 elements, and so on. And we pass the number of elements that we want to include to the Take() method. This returns an IEnumerable<string> rather than an array, and it is deferred: the elements are read when we enumerate it, not when we call Take(). If the array changes in between, the change shows up in the results. Calling ToArray() on it is what gives us an independent copy.
When the goal is to cut the whole array into equal pieces rather than take one slice, we have a separate article on splitting a sequence into chunks with LINQ. For the paging case itself, see our article on pagination in ASP.NET Core Web API.
How Do We Slice an Array With Array.Copy()?
Let’s consider the same scenario, but this time we’ll achieve the result using the Array method Copy():
var posts = new string[] { "post1", "post2", "post3", "post4", "post5", "post6", "post7", "post8",
"post9", "post10" };
var slicedPosts = new string[5];
Array.Copy(posts, 0, slicedPosts, 0, 5);
foreach (var post in slicedPosts)
Console.WriteLine(post); // Outputs the first 5 posts
Here, we initialize a destination array that will be our array slice. And then we call the Copy() method which takes a source array, starting index, destination array, starting destination index (zero because we’re copying to a new array), and a number of elements we want to slice.
As we see, this method prints the same posts as the LINQ one, but here the sliced elements end up in a new array of their own.
Array.Copy() is one of several ways of copying array elements into a new array, and that article compares them.
Now let’s explore a new approach to this operation.
How Do We Slice an Array Without Copying It Using ArraySegment?
For this example let’s consider a new scenario. Let’s say we are building a machine learning model for a charity organization that will predict whether a person would donate or not based on their age. We have a bunch of data that we want to split into two sections: training data, on which the model will train, and testing data, by which we will test our model:
var data = new Tuple<int, bool>[] {
new(20, true),
new(50, true),
new(35, false),
new(55, true),
new(16, false)
};
We want to split these five records into a training set of 3 records, and a testing set of 2 records.
Now, we already know a couple of ways to do this, but let’s introduce a new one, using ArraySegment<T>:
var trainingData = new ArraySegment<Tuple<int, bool>>(data, 0, 3);
var testingData = new ArraySegment<Tuple<int, bool>>(data, 3, 2);
Console.WriteLine("Training Data:");
foreach (var record in trainingData)
Console.WriteLine(record);
Console.WriteLine();
Console.WriteLine("Testing Data:");
foreach (var record in testingData)
Console.WriteLine(record);
We initialize a new instance of ArraySegment, passing the array we want to slice, the starting index, and the number of elements. This creates a wrapper around the array that delimits elements between these specified positions. We can then iterate on this segment and read its values.
The same thing can be achieved using the ArraySegment<T>.Slice() method:
... var arraySegment = new ArraySegment<Tuple<int, bool>>(data); var trainingData = arraySegment.Slice(0, 3); var testingData = arraySegment.Slice(3, 2); ...
We create an ArraySegment that wraps the whole array, then call the Slice() method, which creates another ArraySegment with the specified boundaries that we pass to it as parameters.
Slice() offsets are relative to the segment, not to the array: calling Slice(1, 2) on a segment that starts at index 1 gives a segment starting at index 2 of the original array.
We have to be careful, though, because this is not a new array, it is actually a value type variable that holds a reference to the array, an offset and a count, so any change to the values through the array segment will reflect in the original array:
var trainingData = new ArraySegment<Tuple<int, bool>>(data, 0, 3);
Console.WriteLine("Training Data:");
for (int i = 0; i < trainingData.Count; i++)
{
trainingData[i] = new(40, false);
Console.WriteLine(trainingData[i]);
}
Console.WriteLine();
Console.WriteLine("Original Data:");
foreach (var record in data)
Console.WriteLine(record);
We’ll see that the first 3 elements were changed in the array segment and in the original array too:
Training Data: (40, False) (40, False) (40, False) Original Data: (40, False) (40, False) (40, False) (55, True) (16, False)
We cover ArraySegment<T> in more detail in its own article, including its properties and how two segments compare.
How Do We Slice an Array With Span<T> and ReadOnlySpan<T>?
ReadOnlySpan<T> is the same kind of wrapper with writes blocked at compile time, so the training data cannot be modified through the slice we hand to the algorithm. It does not freeze the array itself: code holding the original array can still change it, and the span reads whatever is there now. Let’s split our data with it:
var trainingData = new ReadOnlySpan<Tuple<int, bool>>(data, 0, 3);
var testingData = new ReadOnlySpan<Tuple<int, bool>>(data, 3, 2);
Console.WriteLine("Training Data:");
foreach (var record in trainingData)
Console.WriteLine(record);
Console.WriteLine();
Console.WriteLine("Testing Data:");
foreach (var record in testingData)
Console.WriteLine(record);
We notice that ReadOnlySpan is very similar to ArraySegment. It takes the array, the starting index, and the number of elements. We can enumerate it, but if we try to change any element through a ReadOnlySpan instance, trainingData in our example, we get a compile-time error.
We will not repeat the example for the Span<T> but in the same way, as we use ReadOnlySpan<T>, we use Span<T> to slice our arrays.
For this scenario, we should go with ReadOnlySpan because we don’t want our data to be changed by our learning algorithm. However, we might have some other algorithms that update our data regularly, in this case, we should use Span<T>.
In practice, we rarely call the span constructor. The AsSpan() extension method creates a span over an array, and Span<T>.Slice() cuts an existing span down further:
var trainingData = data.AsSpan(0, 3); var testingData = data.AsSpan(3); var firstTwo = trainingData.Slice(0, 2); var lastTwo = data.AsSpan()[^2..];
AsSpan(0, 3) takes the first three records and AsSpan(3) takes the rest. Slice(0, 2) narrows the training span to its first two records, and [^2..] takes the last two records of the array. None of these lines allocates a new array, and writing through any of these spans changes data.
Span<T> is a ref struct. It cannot be a field of a class and it cannot survive an await, which is why a slice that has to be stored or passed through asynchronous code uses Memory<T> or ArraySegment<T> instead.
We explain the differences between Span<T> and Memory<T> in a separate article, and another one shows how Span<T> improves performance.
How Do We Slice a List or a String in C#?
List<T> takes the same syntax, but only on recent .NET. list[1..3] compiles from .NET 8 onward, where Slice() was added to List<T>, and it returns a new list holding copies. On .NET 6 and .NET 7 that same line does not compile at all.
GetRange(1, 2) does the same job on every version and also returns a copy.
For a view over a list rather than a copy, CollectionsMarshal.AsSpan(list) hands back a Span<T> over the list’s internal array. It is fast and it is sharp: the list is free to swap that array out when it grows, and the span does not follow.
Strings slice the same way arrays do. text[5..] returns a new string and does exactly what text.Substring(5) does, and an end index past the last character throws an ArgumentOutOfRangeException.
text.AsSpan(5) gives us the same characters with no allocation at all.
The Microsoft Learn page for CollectionsMarshal.AsSpan() puts that rule in one line: “Items should not be added or removed from the List<T> while the Span<T> is in use.”
Which C# Array Slicing Method Should We Use?
The first question is whether we need a copy or a view.
Take a copy when the slice outlives the array, when something else may change the array underneath us, or when we hand the result to code that stores it. The range operator is the shortest way to get one, and Array.Copy() is the one to reach for when the destination array already exists and we are filling part of it.
Take a view when the slice is short lived and allocations matter, which covers most parsing and buffer work. AsSpan() is the default, and ReadOnlySpan<T> is the same thing with writes blocked by the compiler.
Use ArraySegment<T> or Memory<T> when the slice has to live in a field or travel through an async method, because Span<T> can do neither.
C# has no splice method: the JavaScript version removes and returns in one call, and C# keeps those two jobs apart.

The two branches are the two answers to that first question: the copy on the left is a separate array, while the view on the right frames elements of the array we already had, so a write through it lands in the original.
The table lines up every array slicing technique from this article against what it allocates and where its writes go:
| Technique | Copy or view | Allocates a new array | Writes reach the original | Reach for it when |
|---|---|---|---|---|
numbers[1..4] | Copy | Yes | No | We want a subarray in one line |
Array.Copy(src, 1, dest, 0, 3) | Copy | Into an array we supply | No | The destination array already exists |
numbers.Skip(1).Take(3) | Neither until enumerated | No, and nothing is read either | Reads see later changes | We are paging, or chaining more LINQ |
new ArraySegment<T>(numbers, 1, 3) | View | No | Yes | The slice must live in a field or cross an await |
numbers.AsSpan(1, 3) | View | No | Yes | Allocations matter and the slice is short lived |
new ReadOnlySpan<T>(numbers, 1, 3) | View | No | No, the compiler blocks them | We hand a slice to code that must not modify it |
numbers.AsMemory(1, 3) | View | No | Yes | The slice must cross an await and stay a view |
Slicing takes one array apart. The opposite job, merging two arrays into one, has its own set of methods.
Conclusion
In this article, we introduced array slicing in C#. We have implemented a number of scenarios using different approaches and we’ve learned how to slice arrays efficiently.
Tested with .NET 10.0.10.

Since C# 10 we can pass range to the Take method of LINQ
That’s right Magomed.
For those wondering, we can do this now:
var listOfNumbers = new List<int>() { 1, 2, 3, 4, 5, 6, 7 }; var bla = listOfNumbers.Skip(2).Take(2..4);The result would be 5, 6.
It’s probably worth mentioning that the Range Operator approach allocates new arrays when you use it on an array, and does not when you use it on a Span.
“However, we might have some other algorithms that update our data regularly, in this case, we should use
ArraySegment.”I disagree with this. If you need to update data, use Span<T> instead of ReadOnlySpan<T>. There’s few if any scenarios where ArraySegment is better than Span.
Thank you Joel a lot. Those are great suggestions and we have updated the article, you just need to refresh the page if you can’t see the updates (due to caching). Just regarding the part you are not agreeing with – I agree with you, we just didn’t mention Span here so we didn’t choose it over ReadOnlySpan. But, we fixed that. It should be mentioned.