Range Over Iterators in C#

Starting with version 1.23, Go has added support for iterators, which lets us range over pretty much anything!

Let’s look at the List type from the previous example again. In that example, we had an AllElements method that returned a slice of all elements in the list. With Go iterators, we can do it better - as shown below.

using System;
using System.Collections;
using System.Collections.Generic;

public class List<T> : IEnumerable<T>
{
    private class Element<U>
    {
        public U Value;
        public Element<U> Next;
    }

    private Element<T> head, tail;

    public void Push(T value)
    {
        var newElement = new Element<T> { Value = value };
        if (tail == null)
        {
            head = tail = newElement;
        }
        else
        {
            tail.Next = newElement;
            tail = newElement;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        for (var current = head; current != null; current = current.Next)
        {
            yield return current.Value;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public static class Program
{
    public static IEnumerable<int> GenFib()
    {
        int a = 1, b = 1;
        while (true)
        {
            yield return a;
            int temp = a;
            a = b;
            b = temp + b;
        }
    }

    public static void Main()
    {
        var lst = new List<int>();
        lst.Push(10);
        lst.Push(13);
        lst.Push(23);

        foreach (var e in lst)
        {
            Console.WriteLine(e);
        }

        var all = new List<int>(lst);
        Console.WriteLine("all: [" + string.Join(" ", all) + "]");

        foreach (var n in GenFib())
        {
            if (n >= 10)
                break;
            Console.WriteLine(n);
        }
    }
}

List.All returns an iterator, which in Go is a function with a special signature.

The iterator function takes another function as a parameter, called yield by convention (but the name can be arbitrary). It will call yield for every element we want to iterate over, and note yield’s return value for a potential early termination.

Iteration doesn’t require an underlying data structure, and doesn’t even have to be finite! Here’s a function returning an iterator over Fibonacci numbers: it keeps running as long as yield keeps returning true.

Since List implements IEnumerable<T>, we can use it in a regular foreach loop.

Packages like System.Linq have a number of useful functions to work with iterators. For example, ToList() takes any iterator and collects all its values into a list.

Once the loop hits break or an early return, the yield function will return false.