Summary#
Write the C# iterator you would write anywhere else:
IEnumerable<int> Evens(int upTo) {
for (int i = 0; i <= upTo; i += 2) {
yield return i;
}
}
[Test]
void An_Iterator_Hands_Back_Its_Sequence() {
var seen = new List<int>();
foreach (var n in Evens(6)) seen.Add(n);
Assert.Equal(4, seen.Count);
Assert.Equal(12, Evens(6).Sum());
Assert.Equal("4,6", string.Join(",", Evens(6).Where(n => n > 2)));
}Signature#
IEnumerable<T> Name(<params>) {
…
yield return <item>; // add one item to the sequence
…
yield break; // end the sequence here
}Description#
How do I end the sequence early?#
yield break; ends it with what was produced so far — the same as reaching the end of the body:
IEnumerable<int> UpTo(int n) {
for (int i = 0; ; i++) {
if (i >= n) yield break;
yield return i;
}
}
[Test]
void Yield_Break_Ends_The_Sequence() {
Assert.Equal(3, UpTo(3).Count());
}What does the caller hold?#
The whole sequence, as Osy#'s read-only sequence type — so var xs = Evens(10); is an int[] and everything an array
does works on it. A parameter or local may say it holds one as IEnumerable<int> too — the collection interfaces
are the collections they name (IEnumerable<T>, IReadOnlyList<T>, IList<T> — the collection interfaces).
When is the sequence produced?#
When the iterator is CALLED, all at once — C# produces it one item per step of the caller's loop instead. The answer is the same for every iterator that ends; what differs is only WHEN its body runs relative to its reader's.
An iterator that never ends#
An iterator whose loop can never end is meant to be cut short by its reader, and it is — the reader says how many items it needs, and the iterator stops there:
IEnumerable<int> Naturals() {
int i = 0;
while (true) { yield return i++; }
}
[Test]
void An_Endless_Iterator_Is_Cut_Short_By_Its_Reader() {
Assert.Equal(10, Naturals().Take(5).Sum());
Assert.Equal(12, Naturals().Where(x => x % 3 == 0).Select(x => x * 2).First(y => y > 10));
Assert.Equal(4, Naturals().TakeWhile(x => x < 4).Count());
Assert.True(Naturals().Contains(42));
}The readers that can say how much they need are .Take(n) (also .Skip(k).Take(n)), .First(), .FirstOrDefault()
and .Any() — each with or without a predicate — .ElementAt(i), .TakeWhile(…) and .Contains(x), with any
.Where(…) and .Select(…) between the call and the reader. Any other reader of an endless iterator — a foreach, a
.ToList(), a .Count() — would never finish, in C# as here, and is a compile error that names the readers above. A
foreach that breaks out of an endless iterator is one of those: write the stopping point as the reader
(foreach (var n in Naturals().TakeWhile(x => x < 100))).
The iterator METHOD of a class keeps the older rule — its loop needs a way out — because its call sites cannot yet hand it what its reader needs; declare it as a top-level function to read it this way.
A sequence whose items arrive over TIME — a long query, a model's streamed answer — is a different thing with its own
declaration: yield — a function that produces results over time (stream<T>), which a page watches as it fills.
See also#
- yield — a function that produces results over time —
stream<T>: items that arrive over time, to alive var - foreach — reading a sequence item by item
- LINQ over a local list —
Where,Select,Sumand the rest over a sequence in hand