Enumerable.Index and More

With .net 9 a new Enumerable extension was introduced: Enumerable.Index(). This method combines the item from the enumeration with its index. This is a useful addition which avoids manual counting in foreach loops or using for loops directly. In this article I will show how the method is built in detail and will implement similar methods that return whether the item in the enumeration is the first or last item.

The New Enumerable.Index Method

Sometimes we want to enumerate a collection and get an index, for example to print a ranking. In those cases, we used to count an index manually like in the following example:

private static void PrintTop10(IEnumerable<Player> playersTop10Ordered)
{
    int index = 1;

    foreach (Player player in playersTop10Ordered)
    {
        Console.WriteLine($"{index,4}. {player.Points,5} - {player.Name}");

        index++;
    }
}

With .net 9 we have an easier way to index an enumeration, the Enumerable.Index() method. We just apply .Index() to the desired enumeration and we get a ValueTuple for each item:

foreach ((int Index, Player Item) playerItem in playersTop10Ordered.Index())
{
    Console.WriteLine($"{playerItem.Index + 1,4}. {playerItem.Item.Points,5} - {playerItem.Item.Name}");
}

I'm not really convinced by the ValueTuple (a new generic struct would have been much nicer), but I like the general idea of the method. The actual implementation is very simple as expected. It has a separate branch for empty arrays to improve performance and the increment is in a checked context:

public static IEnumerable<(int Index, TSource Item)> Index<TSource>(this IEnumerable<TSource> source)
{
    if (source is null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (IsEmptyArray(source))
    {
        return [];
    }

    return IndexIterator(source);
}

private static IEnumerable<(int Index, TSource Item)> IndexIterator<TSource>(IEnumerable<TSource> source)
{
    int index = -1;
    foreach (TSource element in source)
    {
        checked
        {
            index++;
        }

        yield return (index, element);
    }
}

The index starts with 0 and if there are more than int.MaxValue items in the enumeration, an OverflowException is thrown. The check for empty arrays is in a separate method but is marked to get inlined:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsEmptyArray<TSource>(IEnumerable<TSource> source)
{
    return source is TSource[] { Length: 0 };
}

Implementing the WithIsFirst Method

Sometimes we need to know if the item is the first item in the enumeration. We could use the Index method and check whether the index is 0. A better solution would be to implement a separate method that returns whether the item is the first in the enumeration. We wouldn’t have to increment an int with each turn and it wouldn’t throw an OverflowException if our enumeration has a couple billion items (ok, that won’t happen too often anyways).

The classic solution would look like this with a bool that is managed manually:

private static void PrintWinner(IEnumerable<Player> playersTop10Ordered) {
    bool isFirst = true;

    foreach (Player player in playersTop10Ordered)
    {
        if (isFirst)
        {
            Console.WriteLine($"WINNER: {player.Name}");
            isFirst = false;
        }
        else
        {
            Console.WriteLine($" LOSER: {player.Name}");
        }
    }
}

Our desired solution would look like this where we don’t have to deal with a bool:

private static void PrintWinnerNew(IEnumerable<Player> playersTop10Ordered)
{
    foreach (ItemWithIsFirst<Player> playerItem in playersTop10Ordered.WithIsFirst())
    {
        if (playerItem.IsFirst)
        {
            Console.WriteLine($"WINNER: {playerItem.Item.Name}");
        }
        else
        {
            Console.WriteLine($" LOSER: {playerItem.Item.Name}");
        }
    }
}

Instead of a ValueTuple I use a custom readonly record struct:

public readonly record struct ItemWithIsFirst<T>
{
    public required bool IsFirst { get; init; }
    public required T Item { get; init; }
}

The implementation of the method is similar to Enumerable.Index(). I also included the check for empty arrays and used a copy of the IsEmptyArray method from the original source. Like in the Index() method, I separated the actual iterator logic into a separate method. This way the arguments will get checked at the call site of the method and not when the IEnumerator is executed.

public static IEnumerable<ItemWithIsFirst<T>> WithIsFirst<T>(this IEnumerable<T> source)
{
    ArgumentNullException.ThrowIfNull(source);

    if (IsEmptyArray(source))
    {
        return [];
    }

    return WithIsFirstIterator(source);
}

private static IEnumerable<ItemWithIsFirst<T>> WithIsFirstIterator<T>(IEnumerable<T> source)
{
    bool isFirst = true;

    foreach (T item in source)
    {
        yield return new ItemWithIsFirst<T>
        {
            IsFirst = isFirst,
            Item = item,
        };

        isFirst = false;
    }
}

By using these couple lines, we get easier code at the call site and will never forget to set the bool again.

Implementing the WithIsLast Method

When we want to know if an item is the last in an enumeration, it’s getting more complicated. We can’t just simply use a bool and foreach, because then we don’t have access to the next item. An easy solution would be to convert the enumeration to a list where we know the length and can access the items by index, but this would lead to allocations. We have to go a level deeper and use the IEnumerator directly.

First, we need another new struct with matching names:

public readonly record struct ItemWithIsLast<T>
{
    public required bool IsLast { get; init; }
    public required T Item { get; init; }
}

The code for the method would look like as follows, again with checks for empty arrays and a separate method for the iterator part:

public static IEnumerable<ItemWithIsLast<T>> WithIsLast<T>(this IEnumerable<T> source)
{
    ArgumentNullException.ThrowIfNull(source);

    if (IsEmptyArray(source))
    {
        return [];
    }

    return WithIsFirstLastIterator(source);
}

private static IEnumerable<ItemWithIsLast<T>> WithIsFirstLastIterator<T>(IEnumerable<T> source)
{
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    {
        bool hasMoreItems = enumerator.MoveNext();

        while (hasMoreItems)
        {
            T current = enumerator.Current;

            hasMoreItems = enumerator.MoveNext();

            yield return new ItemWithIsLast<T>()
            {
                IsLast = !hasMoreItems,
                Item = current
            };
        }
    }
}

The method we create a new IEnumerator<T> which is getting disposed if required. Then we try to move the enumerator to the first item and would not enter the while loop with an empty enumeration. In the while loop we get the current value and then move to the next item. By trying to move to the next item, we know whether a next item exists. If not, the current item is the last item. With this information we can yield an item and continue with the next.

Conclusion

The new Enumerable.Index() method is a helpful addition in .net 9 and, if required, easy to port to your own application if an older .net version is used. Note that you maybe have to change the check for empty arrays because newer language features are used in the implementation above.

The WithIsFirst and WithIsLast methods are also helpful and much nicer to use than managing a bool manually.