I recently got an interesting comment on an older post of mine about implementing a Python-like range() function in C#. The commenter posed a solution to the slight feeling of disappointment experienced when you have a foreach
loop and then realise you need to access the loop index, forcing a conversion to a for
loop or manually incrementing a counter. The solution ended up with a syntax like this:
//From this comment: int[] a = new int[] { 1, 2, 3, 4, 5 }; foreach (int i in Range.Array(a)) { // do something with i or a[i] }
Great concept! :) Let’s take it a step further by drawing some more inspiration from Python. Here’s a Python list comprehension that uses the enumerate()
function to loop over tuples of both index and item:
>>> sampleList = ['a','b','c','d'] >>> [(index,item) for index, item in enumerate(sampleList)] [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]
Or in a more C#-like format:
>>> for index, value in enumerate(sampleList): print(index, value) (0, 'a') (1, 'b') (2, 'c') (3, 'd')
Let’s see how this could work in C#. As usual, I’ll start with a test:
[Fact] public void Can_enumerate_index_and_item() { String[] strings = {"a", "b", "c", "d"}; int expectedIndex = 0; foreach (var tuple in Enumerate.Over(strings)) { Assert.Equal(expectedIndex, tuple.Index); Assert.Equal(strings[tuple.Index], tuple.Item); expectedIndex++; } }
To pass this we’ll need a simple Tuple
class (better versions available) and an Enumerate
class (not entirely happy with the names, but we’re just mucking around here). I’m going to violate YAGNI guidelines here and not restrict us to just arrays of Strings.
public class Tuple<T> { public int Index { get; private set; } public T Item { get; private set; } public Tuple(int index, T item) { Index = index; Item = item; } } public class Enumerate { public static IEnumerable<Tuple<T>> Over<T>(IEnumerable<T> items) { int index = 0; foreach (T item in items) { yield return new Tuple<T>(index++, item); } } }
foreach
will return items in the same order as indexed. This isn’t actually guaranteed at all, so you might get cases where the returned index isn’t the actual index of an item.The original test passes (along with a couple of others omitted for brevity), and we can now write a C# version of the Python enumerate()
sample at the start of this post:
String[] strings = { "a", "b", "c", "d" }; foreach (var tuple in Enumerate.Over(strings)) { Console.WriteLine(tuple.Index + ", " + tuple.Item); } /* Outputs: 0, a 1, b 2, c 3, d */
I’m fairly sure there’s a better (LINQ-y?) way of doing this, but this is all my Googling and I can come up with right now. :)