19

With C# 8 we got ranges to get "sub lists".

While this works:

var array = new string[] { "abc", "def", "ghi" };
var subArray = array[0..1]; // works

This does not:

var list = new List<string> { "abc", "def", "ghi" };
var subList = list[0..1]; // does not work

How can I use ranges with lists?

Sebastian Krysmanski
  • 8,114
  • 10
  • 49
  • 91

5 Answers5

17

List does not itself support Ranges. However, if all you have is a Range, you can create a helpful extension method like so:

public static class ListExtensions
{
    public static List<T> GetRange<T>(this List<T> list, Range range)
    {
        var (start, length) = range.GetOffsetAndLength(list.Count);
        return list.GetRange(start, length);
    }
}

Then you can use it to get your sub-List:

var list = new List<string> { "abc", "def", "ghi" };
var subList = list.GetRange(0..1);
yaakov
  • 5,552
  • 35
  • 48
10

Just for completeness, I know this isn't exactly what OP is asking for.

List<T> has a GetRange-Method.

https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.getrange?view=net-5.0

List<?> range = list.GetRange(0,5);

It doesn't have much to do with the new language feature but it does what it says.

Please do not use ToArray only to use that fancy new language feature. It's not worth the overhead.

Alireza Ahmadi
  • 8,579
  • 5
  • 15
  • 42
CSharpie
  • 9,195
  • 4
  • 44
  • 71
5

You can use ToArray() method to solve the problem:

var list = new List<string> { "abc", "def", "ghi" };
var subList = list.ToArray()[0..1]; // works

But note that List<T> has GetRange method that does the same thing:

var list = new List<string> { "abc", "def", "ghi" };
var subList = list.GetRange(0,1);//abc
Alireza Ahmadi
  • 8,579
  • 5
  • 15
  • 42
  • 1
    I was going to validate the post of this comment but you deleted your question, thus I put it here: I don't know if I understand the purpose but extension methods don't support indexed behavior and never will because what extension methods are: [C# extend indexer?](https://stackoverflow.com/questions/67856682/cant-create-an-sqlconnection-in-c-sharp) and [How to use indexers with Extension Methods having out parameter and function calls](https://stackoverflow.com/questions/3117351/how-to-use-indexers-with-extension-methods-having-out-parameter-and-function-cal) –  Jun 06 '21 at 07:36
  • 1
    Thanks Olivier Rogier the second one is very useful link – Alireza Ahmadi Jun 06 '21 at 07:39
  • 1
    @AlirezaAhmadi Most of the time, you say the best shortest answer! :) – Majid M. Jun 06 '21 at 08:02
4

Unfortunately you can't. From the language specification:

For example, the following .NET types support both indices and ranges: String, Span, and ReadOnlySpan. The List supports indices but doesn't support ranges.

Type Support for ranges and indices.

So the only way would be something like this, which is a bit clunky:

var subList = list.ToArray()[0..1];

Edit: I will leave the rest of my answer here for the record, but I think the answer using Range is more elegant, and will be significantly faster, especially for large slices.

Benchmarking and Extension Method

The following demonstrates how much slower using ToArray() can be (using LINQPad - F.Rnd.Str is my own helper method for generating random strings):

var list = new List<string>();
for (int i = 0; i < 100000; i++)
{
    list.Add(F.Rnd.Str);
}

// Using ToArray()
var timer = new Stopwatch();
timer.Start();
var subList0 = list.ToArray()[42..142];
timer.Stop();
timer.ElapsedTicks.Dump("ToArray()");

// Using Indices
timer.Reset();
timer.Start();
var subList1 = new List<string>();
for (int i = 42; i < 142; i++)
{
    subList1.Add(list[i]);
}
timer.Stop();
timer.ElapsedTicks.Dump("Index");

// ToArray()
// 3136
// Index
// 28

Therefore an extension method like this will be very quick:

public static class ListExtensions
{
    public static List<T> GetSlice<T>(this List<T> @this, int first, int last)
    {
        var slice = new List<T>();
        for (int i = first; i < last; i++)
        {
            slice.Add(@this[i]);
        }

        return slice;
    }
}

Then you can add this to the benchmark:

// Using Extension Method
timer.Reset();
timer.Start();
var subList2 = list.GetSlice(42, 142);
timer.Stop();
timer.ElapsedTicks.Dump("Extension");

Benchmark results:

// ToArray()
// 2021
// Index
// 16
// Extension
// 15

Range

One of the answers uses Range to achieve the same thing, so for comparison:

timer.Reset();
timer.Start();
var range = new Range(42, 142);
var (start, length) = range.GetOffsetAndLength(list.Count);
var subList3 = list.GetRange(start, length);
timer.Stop();
timer.ElapsedTicks.Dump("Range");

Benchmark results:

// ToArray()
// 2056
// Index
// 21
// Extension
// 16
// Range
// 17

You can see the bottom three methods are essentially the same in terms of speed (as I run it multiple times they all vary between 15-21 ticks).

bfren
  • 473
  • 3
  • 7
  • your `GetSLice`-Method is alot less efficient than `List.GetRange`. While basically having the exact same functionality. – CSharpie Jun 06 '21 at 06:48
  • Actually it's pretty much identical in terms of run speed, I'll add it to my benchmark. – bfren Jun 06 '21 at 06:53
  • it would be if you intialized the new list with a meaningful capacity, and use Array.Copy like the original does. Your perfoamcnetest is not a really good test. Your extension is massively outperformed by the original. Checkout https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,597 Using a Timer and only do one operation is not a good way to measure performance. – CSharpie Jun 06 '21 at 06:56
  • `ReadOnlySpan subList = CollectionsMarshal.AsSpan(list)[0..1];` – Jodrell Sep 26 '22 at 16:00
3

Rather than converting the List<T> by copying it to array you could access the internals as a Span<T>.

This saves unnecessary allocation and copying.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
                    
public class Program
{
    public static void Main()
    {
        var list = new List<string> { "abc", "def", "ghi" };
        ReadOnlySpan<string> subList = CollectionsMarshal.AsSpan(list)[0..1];
        Console.WriteLine(subList.Length);
        Console.WriteLine(subList[0]);
    }
}
Jodrell
  • 34,946
  • 5
  • 87
  • 124