In this article, we are going to learn more about ranges and indices in C#, and how to use them to access a single or a range of elements in a sequence. We’ll also see how ranges and indices help us write cleaner and more readable code.

To download the source code for this article, you can visit our GitHub repository.

Let’s dig in.

What Are Indices?

In C# indices represent an index in a sequence. Starting from C# 8.0, the ^ operator can be used to specify an index relative to the end of a sequence. The constructor Index is composed of two parameters.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Let’s look at the constructor definition:

public Index (int value, bool fromEnd = false);

The value parameter specifies the index value. Its value must be greater than or equal to zero. The second parameter fromEnd is optional. It indicates if the value of the index is from the start or end.

To better understand indices in C#, let’s create the IndexExamples class and see the different ways we can use the index:

public class IndexExamples
{
    public static string GetFirst(string[] names)
    {
        var index = new Index(0);

        return names[index];
    }

    public static string GetLastMethod1(string[] names)
    {
        var index = new Index(1, true);

        return names[index];
    }
}

GetFirst returns the first element of an array. GetLastMethod1 returns the last element of an array. Although the code works fine, we can further simplify the syntax. 

Let’s add new methods to the IndexExamples class:

public static string GetLastMethod2(string[] names)
{
   return names[^1];
}

public static string GetSecondLast(string[] names)
{
    return names[^2];
}

In both GetLastMethod2 and GetSecondLast we use the hat operator to indicate the index is from the end. GetLastMethod2 and GetLastMethod1 result in the same output, however the syntax in GetLastMethod2 is cleaner.

When we use the index from the end, the index value must be greater than zero. Otherwise, it will throw the System.IndexOutOfRangeException exception. For the index value, the index ^i is equivalent to names.Length - i. 

For example:

  • names[^1] is equivalent to names[names.Length - 1]
  • names[^2] is equivalent to names[names.Length - 2] 

What Are Ranges?

Ranges in C# are a very concise way of representing a subset of a sequence using the .. operator. The range operator specifies the start and the end of a slice. Let’s take x..y as an example to understand the range operator: 

  • x specifies the start index 
  • y specifies the end index 
  • Both x and y are optional values 
  • The range operator (..) is inclusive of the starting index (x) and exclusive of the end index (y) 

Let’s inspect the constructor of the Range class:

public Range(Index start, Index end);

As the name suggests the first parameter defines the start index and the second parameter specifies the end index.

Let’s create the RangeExamples class and see the different ways we can use the range operator(..):  

public class RangeExamples
{
    public static string[] GetAll(string[] arr)
    {
        return arr[..];
    }

    public static string[] GetFirstTwoElements(string[] arr) 
    {
        var start = new Index(0);
        var end = new Index(2);
        var range = new Range(start, end);

        return arr[range];
    }

    public static string[] GetFirstThreeElements(string[] arr) 
    {
        return arr[..3];
    }

    public static string[] GetLastThreeElements(string[] arr) 
    {
        return arr[^3..];
    }

    public static string[] GetThreeElementsFromMiddle(string[] arr) 
    {
        return arr[3..6];
    }
}

The RangeExamples is a class that contains different methods that take advantage of the ..operator. The index feature added in C# 8.0 complements the range feature. It provides an easier way to specify the start and end of the range. For example, in the GetLastThreeElements() method, the hat operator is used to define the range start position.  

Limitations of Ranges and Indices

Ranges and indices provide a succinct syntax for accessing a single element or ranges in a sequence. However, both range and index have required specifications.

To use the index language feature on an instance:

  • The class must have publicly accessible Length or Count property
  • Also, it must have an accessible indexer which takes a single argument of type int
  • If the class contains an indexer with more than one parameter the first parameter should not be Index. If the first parameter is Index, the remaining parameters must be optional

To use the range language feature on an instance:

  • A class must have publicly accessible Length or Count property
  • Also, it must have an accessible method named Slice which has two parameters of type int
  • If a class contains an indexer with more than one parameter the first parameter should not be Range. If it is, the remaining parameters must be optional

Limitation Workaround

We cannot use Range and Index with certain built-in types (such as List<T>) and custom types by default. However, we can easily customize our custom type to support these language features. Let’s create a NameList class that contains a list of names, and see how we can customize it to support Index and Range:

public class NameList : IEnumerable<string>
{
    private List<string> _names;

    public NameList()
    {
        _names = new List<string>();
    }

    public IEnumerator<string> GetEnumerator()
    {
        return _names.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)_names).GetEnumerator();
    }
    
    public void Add(string name) => _names.Add(name);
}

To make the NameList instance indexable, we have to make the object countable. We can do that by simply adding either the Count or the Length property.

For this example, let’s add the Count property in this class:

public int Count => _names.Count;

The next step is to add an indexer member that takes a single argument of type int:

public string this[int index] => _names[index];

At this point, we can use the Index language feature with the instance of NameList class. However, we cannot use Range. Because we haven’t added the Slice method yet. 

Let’s add a Slice method into NameList:

public List<string> Slice(int start, int length)
  => _names
         .Skip(start)
         .Take(length)
         .ToList();

Now the NameList class fulfills the requirements of both Index and Range. And we can use these language features on NameList instance. 

Next, let’s create the NameListExamples class and see the different ways we can use Index and Range on the NameList instance:

public class NameListExamples
{
    public static List<string> GetAll(NameList names)
    {
        return names[..];
    }

    public static List<string> GetFirstTwoElements(NameList names)
    {
        var start = new Index(0);
        var end = new Index(2);
        var range = new Range(start, end);
        return names[range];
    }

    public static List<string> GetFirstThreeElements(NameList names)
    {
        return names[..3];
    }

    public static List<string> GetLastThreeElements(NameList names)
    {
        return names[^3..];
    }

    public static List<string> GetThreeElementsFromMiddle(NameList names)
    {
        return names[3..6];
    }

    public static string GetLastElement(NameList names)
    {
        return names[^1];
    }
}

Here the GetAll method returns a copy of NameList elements using the range operator. The GetFirstTwoElements method uses the instance of Range class and returns the first two elements. Additionally, the GetFirstThreeElements method returns the first three elements. The start index is 0 and the end index is 3.

Furthermore, GetLastThreeElements specifies the third element from the end as a start index and ends in the last element. This method returns the last three elements. GetThreeElementsFromMiddle returns elements starting from the 3rd index until the 6th element. And finally, GetLastElement returns the last element using the hat operator.

Similar to the NameList class ranges and indices can be used with any class that meets the required specification.

Conclusion

In this article, we have learned what Index and Range are in C#, their limitations, and  how to implement a custom type that supports Index and Range.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!