In this article, we are going to talk about array slicing in C#. We are going to explore different ways to slice an array in C# with some examples.
Let’s start.
Introducing Array Slicing in C#
Array slicing in C# is the operation of extracting a subset of elements from an array. This subset is usually defined by a starting index and the number of elements to include in the array slice.
C# has very much evolved recently that we now have a number of ways to slice an array. We’re going to explore all of them in a moment, but first let’s explain the logic behind slicing.
In general, there are two logical ways of slicing an array. One way is by creating a new array and appending the sliced elements to it. The other is creating a wrapper around the array that can hold pointers to certain elements inside the array, without creating a new one.
Now, let’s jump into code to see how to slice arrays in C# in action.
Array Slicing Using LINQ
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 operation returns a new IEnumerable
that we can enumerate.
Using the Copy() Method to Slice Array
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 works the same as the LINQ one. It returns a new array containing the sliced elements.
Now let’s explore a new approach to this operation.
Array Slicing Using ArraySegment<T>
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.
We have to be careful, though, because this is not a new array, it is actually a value type variable that holds pointers to elements’ positions in the array, 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, not only in the array segment but also in the original array:
Training Data: (40, False) (40, False) (40, False) Original Data: (40, False) (40, False) (40, False) (55, True) (16, False)
Utilizing ReadOnlySpan<T> or Span<T> to Slice Arrays
The ArraySegment
slicing is the most memory-efficient so far, but it doesn’t guarantee the consistency of our data. This is where ReadOnlySpan<T>
comes to the rescue. It provides a wrapper that allows only reading from the array:
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>
.
Range Operator (x..y) in C# 8.0+
Starting C# 8.0 we have a new operator which has made slicing very simple syntactically. That’s the range operator x..y
. It allows us to slice the elements between index ‘x’ and index ‘y’; index ‘y’ not included:
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.
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.
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:
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.