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.