Iterators
Iterators provide an easy and comfortable way to provide data for a Sequences.
Iterators are special types of Methods that return a Sequence. Rather then executing all at once and returning a finished list, tough, bits of the iterator are run as needed, whenever a new ite for the sequence is requested.
Note: Since iterators are very similar to regular methods, many topics covered in the Methods topic will apply to them, as well.
This includes the sections on Parameters and Method Body.
Iterator Syntax
Iterators are declared like normal methods, but with three important distinctions:
- Iterator methods must return a Sequence type.
- They must me marked with the
iterator
Member Modifier. - Instead of returning a value, they pass back individual items of the sequence using
yield
.
Of course, any method can return a sequence, simply by declaring a sequence type as their result type. A normal method would need to handle the work of constructing the sequence itself – which can be easy when returning data from, say an Array or a list, but becomes more complicated when the data should be generated dynamically as the sequence is enumerated.
What the iterator syntax adds to the table is that it makes it easy to write linear code that returns one item of a sequence after the other, but allow that code to be executed non-linearly, piece by piece, if and when the sequence is accessed.
This is done via the yield
statement), which is available only in iterators, and replaces result
or the ability to exit
with a value.
While the iterator is declared to return sequence of a specific type, inside it, each use of the yield
statement will yield a single value of that type. For example
method NaturalNumbers: sequence of Integer; iterator;
begin
for i := 1 to Int32.Max do
yield i;
end;
...
for each n in NaturalNumbers do
writeLn(n);
Under the Hood
When an iterator is called, the body of the iterator method is not actually executed to calculate the whole sequence. Instead, a new object is instantiated, representing the sequence. Once code starts to enumerate over the sequence, for example in a for each
loop, the iterator's method body will be executed piece by piece in order to provide the sequence elements.
It will run until the first occurrence of yield
, and that value will be returned as the first value of the sequence – and then stop. As the loop continues to iterate the sequence and asks for the next item, execution resumes at that position, until the next yield
statement is encountered, and so on — until the method body reaches it's end, marking the end of the sequence.
Of course the above example is very simple (and could be easily reproduced without iterators, for example using a For Loop Expression. But iterators allow for arbitrary complexity in code, and as many yield
statements as needed.
For example, one could easily imagine a complex parser that processes data, with many nested if
/then
Clauses or case
Statements that dive deep into a data structure and yield new items for a sequence from many places.
Oxygene;'s iterator syntax allows such complex logic to be expressed as linear flow – leaving it to the compiler to unpack the code so that for each tem of the sequence, execution runs form one yield statement to the next, and no further.
method WeirdSequenceOfNumbers(aExtraWeird: Boolean): sequence of String; iterator;
begin
yield 'One';
yield 'Two';
yield 'Three';
for i := 4 to 20 do
yield i.ToString;
yield 'Twenty-1';
if aExtraWeird then begin
for i := 20 to 90 do
yield (i*3).ToString;
yield "Twohundred and seventyone";
end
else begin
for i := 20 to 90 do
yield i;
end;
end;
Delegating Iteration to a Sequence
The yield
keyword also supports delegation of the iterator to a second sequence, as shown below:
var MoreValues := [3,4,5,6]; // array of Int32, can act as sequence
method SomeIntegers: sequence of Int32; iterator;
begin
yield 1; // adds "1" to the sequence
yield 2; // adds "2" to the sequence
yield MoreValues; // adds "3" thru "6" to the sequence, from the array
yield 7; // adds "7" to the sequence
end;
When the iterator reaches the relevant yield
statement, subsequent items are retrieved from that sequence, until it ends. After that, it continues to the next local yield
statement. One can think of it as shorthand for:
yield 1; // adds "1" to the sequence
yield 2; // adds "2" to the sequence
for each i in MoreValues do
yield i
yield 7; // adds "7" to the sequence
Limitations for Code in Iterators
In general, almost all code constructs can be used in iterators, including complex loops and conditional statements.
Since yielding in an iterator returns back to the caller on every iteration, it's not allowed to yield
from within a protected block such as a locking
statement or a try
/ finally
block, as that would leave an unbalanced Exception Handling Stack.
In addition, while an iterator can use the exit
Statement to prematurely finish it's execution in any place, it may not return a value as part of the exit. Similarly, the result
Expression is not available.
Implementing Asynchronous Sequences
Iterators can implement an Asynchronous Sequences, simply by specifying the appropriate async sequence of X
result type:
method SomeIntegers: async sequence of Int32; iterator;
begin
for i: Integer := 0 to 25 do
yield il;
end;
Asynchronous sequences can then be iterated via an * await for each
Loop.
Static Methods, Visibility and Virtuality and Other Modifiers
Since iterators are methods, they can be made static, and visibility and virtuality apply just as for regular methods.
See Also
- Sequence Types
yield
Statementsfor each
Loopsfor each
Loop Expressionsawait for each
Loop Statements