Iterators and Yield
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is
a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in
the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so and show the
compiler messages that the SDK printed. The project-level imports of the SDK (System, System.Collections.Generic,
System.Linq, System.Threading.Tasks and others) are why those namespaces need no Imports in the examples, and the
root namespace of the scratch project was ShelfDocs.
An iterator is a function (or the Get accessor of a property) that produces a sequence of values one at a time. You
write it as straight-line code with loops and Yield statements, and the compiler rewrites it into a hidden class that
implements IEnumerable(Of T) and IEnumerator(Of T) and remembers where the body stopped. The consumer pulls the next value
with For Each, a LINQ query or a hand-written MoveNext loop, and only then does the body run up to the next Yield. This
is what makes iterators the natural way to produce a sequence that is expensive, large or infinite, or that must be
filtered and transformed step by step without building intermediate lists. The running example is the shelf library
catalogue: books, loans and the stores that hold them.
Iterators were introduced in Visual Basic 11 (Visual Studio 2012), together with Async/Await, and the feature has not
changed in syntax since. Two things are different from C#, and both are covered below. Visual Basic allows Yield inside
the Try block of a Try statement that has Catch clauses, and it has no asynchronous iterators at all: an IAsyncEnumerable(Of T)
can be consumed from Visual Basic, but not written with Yield.
Iterator functions
[modifiers] Iterator Function Name([parameters]) As IEnumerable(Of T) | IEnumerator(Of T) | IEnumerable | IEnumerator
' statements, including one or more Yield expression
End Function
-
The
Iteratormodifier turns aFunctioninto an iterator. There is noIterator Sub: an iterator always returns a sequence. -
The return type must be
IEnumerable(Of T),IEnumerator(Of T)or one of the non-generic formsIEnumerableandIEnumerator.List(Of T), arrays,IntegerandIAsyncEnumerable(Of T)are rejected (BC36938). -
Yield expressionhands one element to the consumer and suspends the body. The expression must convert implicitly to the element type (T, orObjectfor the non-generic forms), under the usualOption Strict Onrules. -
The iterator ends when execution reaches
End Function, anExit Functionor aReturnstatement without a value.Return valueis rejected with BC36942 ("To return a value from an Iterator function, use 'Yield' rather than 'Return'"), because the function produces its values throughYield. -
Yieldis not a reserved word. It has its special meaning only inside an iterator, and in an ordinary procedureYield 3is a syntax error (BC30800, because the compiler readsYieldas a procedure name). -
An iterator cannot have
ByRefparameters (BC36927), because the state machine keeps the parameters in fields. It cannot be an event accessor, an instance constructor or a static constructor. -
Iteratorcan be combined withPublic,Private,Shared,Overridable,OverridesandImplements, like any other function. It cannot be combined withAsync(BC36936).
The first example shows the central property of an iterator: calling it runs none of its body. The Console.WriteLine
calls expose when each part of the body runs.
Option Strict On
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
End Class
Public Class Catalog
Private ReadOnly _books As New List(Of Book)
Public Sub Add(book As Book)
_books.Add(book)
End Sub
' An iterator function: the compiler turns it into a state machine.
Public Iterator Function BooksPublishedSince(year As Integer) As IEnumerable(Of Book)
Console.WriteLine(" [iterator starts]")
For Each book In _books
Console.WriteLine(" [looking at " & book.Title & "]")
If book.Year >= year Then
Yield book
End If
Next
Console.WriteLine(" [iterator ends]")
End Function
End Class
Module Program
Sub Main()
Dim catalog As New Catalog()
catalog.Add(New Book("Compilers", 1986))
catalog.Add(New Book("Refactoring", 1999))
catalog.Add(New Book("Domain-Driven Design", 2003))
Console.WriteLine("Calling the iterator function...")
Dim recent = catalog.BooksPublishedSince(1999)
Console.WriteLine("...it has returned; nothing ran yet.")
Console.WriteLine("Starting the For Each:")
For Each book In recent
Console.WriteLine("got " & book.Title)
Next
End Sub
End Module
Calling the iterator function...
...it has returned; nothing ran yet.
Starting the For Each:
[iterator starts]
[looking at Compilers]
[looking at Refactoring]
got Refactoring
[looking at Domain-Driven Design]
got Domain-Driven Design
[iterator ends]
Source: Yield Statement
Read the output from the top. The call catalog.BooksPublishedSince(1999) returned a recent variable and printed nothing
else, because it only constructs the state machine and stores the arguments. The first MoveNext, called by For Each,
ran the body up to the first Yield. Each later MoveNext resumed after the Yield that had last run, with every local
variable, the position in the inner For Each and the argument year intact, and ran to the next Yield. The line
[iterator ends] appeared only when the loop asked for one more element after the last book.
Deferred execution
Because the body runs on demand, an iterator gives you deferred execution (lazy evaluation), the same model as the LINQ operators in LINQ and Query Expressions. The consequences are worth stating exactly, because they cause most of the surprises in practice.
-
Nothing runs when the iterator function is called, including argument checks and any side effect.
-
Every new enumeration runs the body again from the start. An expensive iterator that is consumed twice does the work twice.
-
The body reads the data it uses (a list, a field, a captured variable) when the loop reaches it, not when the iterator function was called.
-
If the underlying collection changes while an enumeration is under way, the collection decides what happens;
List(Of T)throwsInvalidOperationException.
Option Strict On
Module Program
Private _calls As Integer
Private Iterator Function Numbered(count As Integer) As IEnumerable(Of Integer)
_calls += 1
Dim run = _calls
For i = 1 To count
Console.WriteLine(" run " & run & " produces " & i)
Yield i
Next
End Function
Sub Main()
' 1. The sequence is re-created on every enumeration.
Dim numbers = Numbered(2)
Console.WriteLine("first Count: " & numbers.Count())
Console.WriteLine("second Count: " & numbers.Count())
' 2. Materialise once with ToList when the work is expensive or has side effects.
Dim snapshot = Numbered(2).ToList()
Console.WriteLine("snapshot: " & snapshot.Count & " and " & snapshot.Count)
' 3. The iterator sees the source as it is when the loop reaches it,
' not as it was when the iterator function was called.
Dim source As New List(Of String) From {"a", "b"}
Dim upper = Upcase(source)
source.Add("c")
Console.WriteLine("late binding: " & String.Join(",", upper))
' 4. Changing the list that is being iterated is detected by the list.
Try
For Each s In Upcase(source)
If s = "A" Then source.Add("d")
Next
Catch ex As InvalidOperationException
Console.WriteLine("InvalidOperationException: " & ex.Message)
End Try
End Sub
Private Iterator Function Upcase(items As List(Of String)) As IEnumerable(Of String)
For Each item In items
Yield item.ToUpperInvariant()
Next
End Function
End Module
run 1 produces 1
run 1 produces 2
first Count: 2
run 2 produces 1
run 2 produces 2
second Count: 2
run 3 produces 1
run 3 produces 2
snapshot: 2 and 2
late binding: A,B,C
InvalidOperationException: Collection was modified; enumeration operation may not execute.
Source: Iterators (Visual Basic)
numbers.Count() ran the body twice, once for each call, and the counter _calls proves it: the second enumeration is
"run 2". Numbered(2).ToList() ran it a third time and stored the two numbers, so the two following snapshot.Count
reads cost nothing. The upper variable was created before "c" was added to source and still saw "c". The fourth
case is the List(Of T) safeguard: adding an item to the list that an iterator is walking invalidates the iterator’s
inner For Each, and the exception arrives from the consumer’s MoveNext call.
Materialise with ToList, ToArray or ToDictionary when you need a snapshot, when the source is a one-shot stream
(a StreamReader, a database reader, a network response) that cannot be read twice, or when the loop body modifies the source.
Code-analysis tools such as ReSharper warn about "possible multiple enumeration" of an IEnumerable(Of T) for this
reason.
Iterators compose into lazy pipelines
An iterator that consumes another sequence pulls its elements on demand, so a chain of iterators moves one element at a
time through every stage. This is how Where, Select and Take in System.Linq are built, and why Take(10) on an
infinite source terminates.
Option Strict On
Module Program
Private Iterator Function Source() As IEnumerable(Of Integer)
For i = 1 To 4
Console.WriteLine("source yields " & i)
Yield i
Next
End Function
Private Iterator Function Filter(items As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
For Each item In items
If item Mod 2 = 0 Then
Console.WriteLine("filter passes " & item)
Yield item
End If
Next
End Function
Private Iterator Function Square(items As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
For Each item In items
Console.WriteLine("square yields " & item * item)
Yield item * item
Next
End Function
Sub Main()
' One element travels through the whole pipeline before the next one starts.
For Each value In Square(Filter(Source()))
Console.WriteLine("consumer got " & value)
Next
End Sub
End Module
source yields 1
source yields 2
filter passes 2
square yields 4
consumer got 4
source yields 3
source yields 4
filter passes 4
square yields 16
consumer got 16
Source: Iterators (Visual Basic)
No stage holds more than one element. A filter that rejects 1 and 3 never lets them reach Square, and the consumer
receives 4 before the source has produced 3. Memory use stays flat however long the source is, which is the reason to
prefer an iterator to a function that builds and returns a List(Of T) when the consumer may stop early or the result may be
large.
Validating arguments
An argument check inside an iterator is itself deferred. LazyTake(Nothing, 1) does not throw at the call; it throws when
the first element is requested, possibly far from the code that passed the wrong argument. When the check should fail at the
call, split the function in two: an ordinary function that validates and returns an iterator. Visual Basic has no local
functions, so the iterator becomes a second, private function.
Option Strict On
Module Program
' Lazy validation: the Throw runs on the first MoveNext, not at the call.
Private Iterator Function LazyTake(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
If count < 0 Then Throw New ArgumentOutOfRangeException(NameOf(count))
Dim taken = 0
For Each item In source
If taken = count Then Exit For
taken += 1
Yield item
Next
End Function
' Eager validation: an ordinary function checks, then delegates to a private iterator.
Private Function EagerTake(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
If count < 0 Then Throw New ArgumentOutOfRangeException(NameOf(count))
Return TakeCore(source, count)
End Function
Private Iterator Function TakeCore(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
Dim taken = 0
For Each item In source
If taken = count Then Exit For
taken += 1
Yield item
Next
End Function
Sub Main()
Try
Dim lazy = LazyTake(Nothing, 1)
Console.WriteLine("LazyTake returned without complaint")
For Each s In lazy
Console.WriteLine(s)
Next
Catch ex As ArgumentNullException
Console.WriteLine("LazyTake threw on enumeration: " & ex.ParamName)
End Try
Try
Dim eager = EagerTake(Nothing, 1)
Console.WriteLine("not reached")
Catch ex As ArgumentNullException
Console.WriteLine("EagerTake threw at the call: " & ex.ParamName)
End Try
Console.WriteLine(String.Join(",", EagerTake({"a", "b", "c"}, 2)))
End Sub
End Module
LazyTake returned without complaint
LazyTake threw on enumeration: source
EagerTake threw at the call: source
a,b
Source: Iterator
ex.ParamName is source in both cases; what differs is where the exception surfaces. The library’s own operators (Take,
Where, Select) use the second pattern, which is why Enumerable.Where(Nothing, ...) throws immediately even though the
filtering itself is deferred.
Iterator properties and enumerable classes
The Get accessor of a property can be an iterator, which is the Visual Basic way to expose a computed sequence as a
property. The property must be ReadOnly and its type must be one of the four sequence types. The Iterator modifier goes
in the property declaration, next to ReadOnly; the Yield statements go in the Get block. A property with parameters
works as well.
A class becomes usable in For Each when it has a GetEnumerator method that returns an enumerator, and the usual
way to say so is to implement IEnumerable(Of T). Making GetEnumerator an iterator avoids writing an enumerator class by
hand (with its MoveNext, Current, Reset and Dispose members). The non-generic IEnumerable.GetEnumerator has to
be implemented too; it forwards to the generic one.
Option Strict On
Public Class Loan
Public ReadOnly Property Borrower As String
Public ReadOnly Property DueOn As Date
Public Sub New(borrower As String, dueOn As Date)
Me.Borrower = borrower
Me.DueOn = dueOn
End Sub
End Class
' Making a class usable in For Each: implement IEnumerable(Of T) with an iterator GetEnumerator.
Public Class LoanBook
Implements IEnumerable(Of Loan)
Private ReadOnly _loans As New List(Of Loan)
Public Sub Add(loan As Loan)
_loans.Add(loan)
End Sub
Public Iterator Function GetEnumerator() As IEnumerator(Of Loan) Implements IEnumerable(Of Loan).GetEnumerator
For Each loan In _loans
Yield loan
Next
End Function
Private Function GetNonGenericEnumerator() As Collections.IEnumerator Implements Collections.IEnumerable.GetEnumerator
Return GetEnumerator()
End Function
' An iterator property: ReadOnly, no Set, the body is the Get accessor.
Public ReadOnly Iterator Property Overdue(today As Date) As IEnumerable(Of Loan)
Get
For Each loan In _loans
If loan.DueOn < today Then Yield loan
Next
End Get
End Property
' A parameterless iterator property.
Public ReadOnly Iterator Property Borrowers As IEnumerable(Of String)
Get
For Each loan In _loans
Yield loan.Borrower
Next
End Get
End Property
End Class
Module Program
Sub Main()
Dim loans As New LoanBook()
loans.Add(New Loan("Ana", #2026-09-01#))
loans.Add(New Loan("Luis", #2026-11-01#))
loans.Add(New Loan("Marta", #2026-10-01#))
For Each loan In loans
Console.WriteLine(loan.Borrower & " due " & loan.DueOn.ToString("yyyy-MM-dd"))
Next
Console.WriteLine("Overdue on 2026-10-19: " & String.Join(", ", loans.Overdue(#2026-10-19#).Select(Function(l) l.Borrower)))
Console.WriteLine("Borrowers: " & String.Join(", ", loans.Borrowers))
Console.WriteLine("LINQ works too: " & loans.Count(Function(l) l.DueOn.Month = 10))
End Sub
End Module
Ana due 2026-09-01
Luis due 2026-11-01
Marta due 2026-10-01
Overdue on 2026-10-19: Ana, Marta
Borrowers: Ana, Luis, Marta
LINQ works too: 1
Source: Iterator
-
LoanBookis enumerable withFor Eachand, because it implementsIEnumerable(Of Loan), with every LINQ operator (Count,Where,OrderBy, …). -
Overdue(today)is an iterator property with a parameter, andBorrowersone without. Both areReadOnly Iterator Property; the order of the two modifiers is free. -
The
GetEnumeratoriteratorImplements IEnumerable(Of Loan).GetEnumerator. The non-generic method isPrivateso that it does not show up in IntelliSense. -
An iterator cannot be used in an event accessor, instance constructor or static constructor, so a class that wants to produce a sequence on construction has to do it from a method or property.
Interfaces, overriding and structures
An iterator function can implement an interface member and can be Overridable; an override can call the base iterator
through MyBase, and its result is just another sequence that the override extends or filters. In a Structure, the
iterator works on a copy of the value: the state machine copies Me when the iterator function is called.
Option Strict On
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
End Class
Public Interface ICatalogStore
Function FindByYear(year As Integer) As IEnumerable(Of Book)
End Interface
Public Class InMemoryStore
Implements ICatalogStore
Private ReadOnly _books As New List(Of Book)
Public Sub New(ParamArray books As Book())
_books.AddRange(books)
End Sub
' An iterator function can implement an interface member.
Public Overridable Iterator Function FindByYear(year As Integer) As IEnumerable(Of Book) Implements ICatalogStore.FindByYear
For Each book In _books
If book.Year = year Then Yield book
Next
End Function
End Class
Public Class ReprintStore
Inherits InMemoryStore
Public Sub New(ParamArray books As Book())
MyBase.New(books)
End Sub
' An overriding iterator can call the base iterator through MyBase and add to its results.
Public Overrides Iterator Function FindByYear(year As Integer) As IEnumerable(Of Book)
For Each book In MyBase.FindByYear(year)
Yield book
Next
Yield New Book("(reprint of " & year & ")", year)
End Function
End Class
Module Program
Sub Main()
Dim store As ICatalogStore = New InMemoryStore(
New Book("Refactoring", 1999), New Book("Compilers", 1986), New Book("Patterns", 1999))
For Each book In store.FindByYear(1999)
Console.WriteLine(book.Title)
Next
Dim reprints As ICatalogStore = New ReprintStore(New Book("Compilers", 1986))
Console.WriteLine(String.Join("; ", reprints.FindByYear(1986).Select(Function(b) b.Title)))
End Sub
End Module
Refactoring
Patterns
Compilers; (reprint of 1986)
Source: Iterator
Option Strict On
Public Structure Window
Public Start As Integer
Public Iterator Function Values() As IEnumerable(Of Integer)
Yield Start
Yield Start + 1
End Function
End Structure
Public Class WindowClass
Public Start As Integer
Public Iterator Function Values() As IEnumerable(Of Integer)
Yield Start
Yield Start + 1
End Function
End Class
Module Program
Sub Main()
Dim w As New Window With {.Start = 5}
Dim fromStructure = w.Values()
w.Start = 100
Console.WriteLine("structure: " & String.Join(",", fromStructure))
Dim c As New WindowClass With {.Start = 5}
Dim fromClass = c.Values()
c.Start = 100
Console.WriteLine("class: " & String.Join(",", fromClass))
End Sub
End Module
structure: 5,6
class: 100,101
Source: Iterator
For the class, the iterator reads Start when the first MoveNext runs, so it sees the later assignment (100,101). For
the structure, the copy of Me was made when w.Values() was called, so the later assignment is not seen (5,6). If a
structure’s iterator must see later changes, make the type a class, or have the consumer call the iterator after the
changes (see Structures and Value Types).
Return types and what the compiler generates
The four return types differ in what they hand back, and in how the caller uses the result.
| Return type | Element type of Yield |
Use |
|---|---|---|
|
|
The normal choice: the result works with |
|
|
A |
|
|
Non-generic collections, |
|
|
A non-generic |
Option Strict On
Module Program
Private Iterator Function Generic() As IEnumerable(Of Integer)
Yield 1
Yield 2
End Function
Private Iterator Function NonGeneric() As IEnumerable
Yield 1
Yield "two"
Yield 3.0
End Function
Private Iterator Function EnumeratorOnly() As IEnumerator(Of Integer)
Yield 1
End Function
Sub Main()
' What the compiler returns is a hidden class implementing both interfaces.
Dim sequence = Generic()
Console.WriteLine("is IEnumerator(Of Integer): " & (TypeOf sequence Is IEnumerator(Of Integer)))
Console.WriteLine("GetEnumerator returns the same object the first time: " &
ReferenceEquals(sequence, sequence.GetEnumerator()))
Console.WriteLine("... and a new object the second time: " &
Not ReferenceEquals(sequence, sequence.GetEnumerator()))
' Current before the first MoveNext is the default value; after the end it keeps the last value (do not rely on either).
Dim e = Generic().GetEnumerator()
Console.WriteLine("before MoveNext: " & e.Current)
While e.MoveNext()
End While
Console.WriteLine("after the end: " & e.Current & " (MoveNext = " & e.MoveNext() & ")")
' Reset is not supported.
Try
e.Reset()
Catch ex As NotSupportedException
Console.WriteLine("Reset: NotSupportedException")
End Try
' The non-generic form yields Object, so the loop variable is Object.
For Each item In NonGeneric()
Console.WriteLine(item.GetType().Name & ": " & item.ToString())
Next
' An iterator that returns IEnumerator is not itself usable in For Each.
Dim only = EnumeratorOnly()
Console.WriteLine("EnumeratorOnly is IEnumerable: " & (TypeOf only Is IEnumerable))
End Sub
End Module
is IEnumerator(Of Integer): True
GetEnumerator returns the same object the first time: True
... and a new object the second time: True
before MoveNext: 0
after the end: 2 (MoveNext = False)
Reset: NotSupportedException
Int32: 1
String: two
Double: 3
EnumeratorOnly is IEnumerable: False
Source: IEnumerator<T> Interface
-
The hidden class implements both
IEnumerable(Of T)andIEnumerator(Of T). The first call toGetEnumeratoron the thread that created the sequence returns the same object, and any later call returns a new, independent enumerator. Do not depend on this; it is an optimisation, not a contract. -
Currentis meaningful only between aMoveNextthat returnedTrueand the nextMoveNext. Before the firstMoveNextit holds the default value, and the value after the end is unspecified. -
ResetthrowsNotSupportedException: ask theIEnumerable(Of T)for a new enumerator instead. -
An iterator that returns
IEnumerator(Of T)is only an enumerator.For Eachneeds anIEnumerable, so a function that returnsIEnumeratorcan be used fromFor Eachonly through a class that exposes it fromGetEnumerator.
The Yield expression has to convert implicitly to the element type. Widening is fine (Yield an Integer in a sequence of
Long), narrowing is not under Option Strict On:
Option Strict On
Module Program
Private Iterator Function Lossy() As IEnumerable(Of Integer)
Dim half As Double = 2.5
Yield half
Yield "3"
End Function
Sub Main()
For Each n In Lossy()
Console.WriteLine(n)
Next
End Sub
End Module
Program.vb(6,15): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(7,15): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.
Source: Yield Statement
Compile-time rules
The compiler checks the shape of an iterator in two passes. The signature errors come first, then the errors about what the body contains; the second kind appears only after the first kind is fixed, so a file can fail twice. Both examples below were built exactly as listed and print the compiler’s messages; the line numbers refer to the listings.
Option Strict On
Module Program
Iterator Function ByRefParameter(ByRef x As Integer) As IEnumerable(Of Integer)
Yield x
End Function
Iterator Function WrongReturnType() As List(Of Integer)
Yield 1
End Function
Iterator Function ScalarReturn() As Integer
Yield 1
End Function
Async Iterator Function Both() As IAsyncEnumerable(Of Integer)
Yield 1
End Function
Sub Main()
End Sub
End Module
Program.vb(4,38): error BC36927: Iterator methods cannot have ByRef parameters.
Program.vb(8,44): error BC36938: Iterator functions must return either IEnumerable(Of T), or IEnumerator(Of T), or the non-generic forms IEnumerable or IEnumerator.
Program.vb(12,41): error BC36938: Iterator functions must return either IEnumerable(Of T), or IEnumerator(Of T), or the non-generic forms IEnumerable or IEnumerator.
Program.vb(16,5): error BC36936: 'Async' and 'Iterator' modifiers cannot be used together.
Source: Iterator
With those four fixed, the body checks run:
Option Strict On
Module Program
Iterator Function InCatch() As IEnumerable(Of Integer)
Try
Yield 1
Catch ex As Exception
Yield 2
End Try
End Function
Iterator Function InFinally() As IEnumerable(Of Integer)
Try
Yield 1
Finally
Yield 2
End Try
End Function
Sub Main()
End Sub
End Module
Program.vb(8,13): error BC36939: 'Yield' cannot be used inside a 'Catch' statement or a 'Finally' statement.
Program.vb(16,13): error BC36939: 'Yield' cannot be used inside a 'Catch' statement or a 'Finally' statement.
Source: Yield Statement
| Error | Meaning | Fix |
|---|---|---|
BC36942 |
|
Use |
BC36927 |
An iterator method cannot have |
Pass |
BC36938 |
The return type is not one of the four sequence types |
Return |
BC36936 |
|
Visual Basic has no asynchronous iterators; see the last section of this page |
BC36939 |
|
Move the |
BC32023 |
|
Use the explicit protocol in the last section |
BC30512 |
The |
Convert explicitly, as everywhere under |
BC36674, BC30205 |
The lambda was written on one line |
An iterator lambda must be multi-line with |
Try, Catch, Finally and Using inside iterators
A Yield can appear in the Try block of a Try statement, and that statement may have Catch clauses and a Finally
block. A Yield inside a Catch or a Finally block is BC36939. This is more permissive than C#, where yield return is
rejected in any try block that has a catch, and as strict as C# for catch and finally blocks themselves.
What each construct does when the iterator is suspended or abandoned:
-
A
Finallyblock and aUsingblock run when the iterator finishes normally, when the consumer leaves the loop early (Exit For,Return, an exception in the loop body, a LINQ operator such asFirstorTakethat stops reading), and whenDisposeis called on the enumerator.For Eachand the LINQ operators callDisposefor you. -
They do not run if the consumer drives the enumerator by hand and never disposes it. A manual
GetEnumerator/MoveNextloop needs its ownUsing(orTry/Finally). -
A
Catchclause in the iterator catches only exceptions raised by the iterator’s own code, such as theInteger.Parsecall below. An exception thrown by the consumer’s loop body does not travel into the iterator.
Option Strict On
Public Class Reader
Implements IDisposable
Public Sub New()
Console.WriteLine(" reader opened")
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Console.WriteLine(" reader closed")
End Sub
Public Function ReadLine(index As Integer) As String
Return "line " & index
End Function
End Class
Module Program
' Finally and Using run when the consumer stops early, because For Each disposes the enumerator.
Private Iterator Function Lines(count As Integer) As IEnumerable(Of String)
Using reader As New Reader()
Try
For i = 1 To count
Yield reader.ReadLine(i)
Next
Finally
Console.WriteLine(" finally block")
End Try
End Using
End Function
' Yield is allowed in the Try block of a Try...Catch, and the Catch sees only exceptions from the iterator's own code.
Private Iterator Function Parsed(texts As IEnumerable(Of String)) As IEnumerable(Of Integer)
For Each entry In texts
Dim value As Integer
Try
value = Integer.Parse(entry)
Yield value
Catch ex As FormatException
Console.WriteLine(" skipped '" & entry & "'")
End Try
Next
End Function
Sub Main()
Console.WriteLine("Full enumeration:")
For Each line In Lines(2)
Console.WriteLine(line)
Next
Console.WriteLine("Exit For after the first item:")
For Each line In Lines(5)
Console.WriteLine(line)
Exit For
Next
Console.WriteLine("First() stops early too:")
Console.WriteLine(Lines(5).First())
Console.WriteLine("Manual enumerator, never disposed -> nothing is cleaned up:")
Dim e = Lines(5).GetEnumerator()
e.MoveNext()
Console.WriteLine(e.Current)
Console.WriteLine(" (no Dispose call, so no 'finally block' line)")
Console.WriteLine("The loop body throws: the iterator's Finally and Using still run:")
Try
For Each line In Lines(3)
Throw New InvalidOperationException("consumer failed on " & line)
Next
Catch ex As InvalidOperationException
Console.WriteLine(" caught: " & ex.Message)
End Try
Console.WriteLine("Catch around Yield:")
Console.WriteLine(String.Join(",", Parsed({"1", "x", "3"})))
End Sub
End Module
Full enumeration:
reader opened
line 1
line 2
finally block
reader closed
Exit For after the first item:
reader opened
line 1
finally block
reader closed
First() stops early too:
reader opened
finally block
reader closed
line 1
Manual enumerator, never disposed -> nothing is cleaned up:
reader opened
line 1
(no Dispose call, so no 'finally block' line)
The loop body throws: the iterator's Finally and Using still run:
reader opened
finally block
reader closed
caught: consumer failed on line 1
Catch around Yield:
skipped 'x'
1,3
Source: Yield Statement
Compare the output of Exit For, First() and the manual enumerator. In the first two the cleanup (finally block, reader
closed) runs before the program continues. In the First() case it even prints before the value is written, because
First disposes the enumerator before it returns the element. The manual enumerator that was never disposed leaves the reader
open, which for a FileStream or a database connection means a leak.
Dispose and MoveNext: the 17.13 change
An enumerator that was disposed must not run any more of the iterator’s code. Before Visual Basic 17.13 (the compiler
that shipped with Visual Studio 2022 17.13) the state machine of an iterator that returned IEnumerator(Of T) could be resumed
after Dispose: according to the compiler team’s description, execution could continue after the point where the
iterator had been suspended. The compiler team listed this fix
in the breaking changes for the .NET 10 SDK ("Set state of enumerator object to 'after' during disposal"). From that
version on, MoveNext on a disposed enumerator returns False without running any user code.
Option Strict On
Module Program
Private Iterator Function Steps() As IEnumerator(Of Integer)
Console.WriteLine(" running step 1")
Yield 1
Console.WriteLine(" running code between the yields")
Yield 2
Console.WriteLine(" running step 3")
Yield 3
End Function
Private Iterator Function WithCleanup() As IEnumerator(Of Integer)
Try
Yield 1
Yield 2
Finally
Console.WriteLine(" cleanup")
End Try
End Function
Sub Main()
Dim e = Steps()
Console.WriteLine("MoveNext: " & e.MoveNext() & ", Current = " & e.Current)
e.Dispose()
Console.WriteLine("MoveNext after Dispose: " & e.MoveNext())
Dim f = WithCleanup()
f.MoveNext()
f.Dispose()
f.Dispose()
Console.WriteLine("MoveNext after second Dispose: " & f.MoveNext())
' A disposed iterator that never started never runs its body either.
Dim g = Steps()
g.Dispose()
Console.WriteLine("MoveNext on a never-started, disposed iterator: " & g.MoveNext())
End Sub
End Module
running step 1
MoveNext: True, Current = 1
MoveNext after Dispose: False
cleanup
MoveNext after second Dispose: False
MoveNext on a never-started, disposed iterator: False
Source: IDisposable Interface
-
The first
MoveNextreturnsTrue; theDisposethen ends the sequence. The nextMoveNextreturnsFalseand the line "running code between the yields" is never printed. That line is what a resumed state machine would have run. -
Dispose runs the pending
Finallyblocks once. A secondDisposeis harmless. -
Code that only uses
For Eachand LINQ never sees the difference, because those neverMoveNextafterDispose. The change affects hand-written loops and wrappers that keep an enumerator around after disposing it. If a program depended on the old behaviour, it relied on the state of a disposed object. The change is a compiler behaviour fix recorded in the breaking-changes document, not a new syntax feature.
Recursion, infinite sequences, generics and extension methods
Iterators can call themselves, take generic type parameters, be <Extension> methods and never end. A recursive iterator
does not flatten for you: the Yield has to re-yield each element of the nested call, so a deep tree costs one nested
MoveNext call per level for every element. Visual Basic has no Yield From or yield*.
Option Strict On
Imports System.Runtime.CompilerServices
Public Module SequenceExtensions
' A generic iterator extension method: chunks of at most "size" items.
<Extension>
Public Iterator Function Batch(Of T)(source As IEnumerable(Of T), size As Integer) As IEnumerable(Of List(Of T))
Dim current As New List(Of T)(size)
For Each item In source
current.Add(item)
If current.Count = size Then
Yield current
current = New List(Of T)(size)
End If
Next
If current.Count > 0 Then Yield current
End Function
End Module
Public Class Category
Public ReadOnly Property Name As String
Public ReadOnly Property Children As New List(Of Category)
Public Sub New(name As String)
Me.Name = name
End Sub
' Recursion: an iterator re-yields what a recursive call produces, level by level.
Public Iterator Function Descendants(Optional depth As Integer = 0) As IEnumerable(Of String)
Yield New String(" "c, depth * 2) & Name
For Each child In Children
For Each line In child.Descendants(depth + 1)
Yield line
Next
Next
End Function
End Class
Module Program
' An infinite sequence is fine as long as the consumer stops.
Private Iterator Function Fibonacci() As IEnumerable(Of Long)
Dim a As Long = 0
Dim b As Long = 1
Do
Yield a
Dim next1 = a + b
a = b
b = next1
Loop
End Function
Sub Main()
Console.WriteLine(String.Join(" ", Fibonacci().Take(10)))
Console.WriteLine("first Fibonacci above 1000: " & Fibonacci().First(Function(n) n > 1000))
For Each group In Enumerable.Range(1, 7).Batch(3)
Console.WriteLine("[" & String.Join(",", group) & "]")
Next
Dim root As New Category("Computing")
Dim languages As New Category("Languages")
languages.Children.Add(New Category("Visual Basic"))
languages.Children.Add(New Category("C#"))
root.Children.Add(languages)
root.Children.Add(New Category("Databases"))
For Each line In root.Descendants()
Console.WriteLine(line)
Next
' Driving an enumerator by hand is what For Each does for you.
Using e = Fibonacci().GetEnumerator()
e.MoveNext()
e.MoveNext()
e.MoveNext()
Console.WriteLine("third value: " & e.Current)
End Using
End Sub
End Module
0 1 1 2 3 5 8 13 21 34
first Fibonacci above 1000: 1597
[1,2,3]
[4,5,6]
[7]
Computing
Languages
Visual Basic
C#
Databases
third value: 1
Source: Iterators (Visual Basic)
-
Fibonaccinever ends, andTake(10)andFirst(...)stop it. An infinite iterator must be consumed only through something that stops (Take,TakeWhile,First,Exit For);ToList()orCount()on it never returns. -
Batch(Of T)is an iterator extension method. Declare it in aModule, with<Extension>and theSystem.Runtime.CompilerServicesimport; see Extension Methods and Partial Methods. .NET 6 addedEnumerable.Chunk, which does the same job and returns arrays;Batchreturns lists. -
In
Descendantsthe inner loop variable is calledline. Naming itnameis refused with BC30039 ("Loop control variable cannot be a property or a late-bound indexed array"), because Visual Basic names are not case sensitive andnameis theNameproperty of the class: inside a class, the property wins over a newFor Eachvariable of the same spelling. The error was reproduced while writing this page.
Iterator lambdas
A lambda can be an iterator. Put Iterator before Function, declare the return type with As, and write the body
on several lines up to End Function. There is no single-line form (BC36674 followed by BC30205), because a Yield
is a statement and a single-line lambda body is an expression. The lambda converts to a delegate type such as
Func(Of Integer, IEnumerable(Of Integer)) in the usual way, see
Delegates and Lambda Expressions.
Option Strict On
Module Program
Sub Main()
' A multi-line iterator lambda. The Iterator keyword goes before Function.
Dim countdown = Iterator Function(start As Integer) As IEnumerable(Of Integer)
For i = start To 1 Step -1
Yield i
Next
End Function
Console.WriteLine(String.Join(" ", countdown(3)))
' A closure captures a local variable; the iterator reads it when it runs, not when it is created.
Dim limit = 2
Dim upToLimit = Iterator Function() As IEnumerable(Of Integer)
For i = 1 To limit
Yield i
Next
End Function
Dim sequence = upToLimit()
limit = 4
Console.WriteLine(String.Join(" ", sequence))
' Iterator lambdas are usable wherever a delegate of the right type is expected.
Dim words As Func(Of String, IEnumerable(Of Char)) =
Iterator Function(w As String) As IEnumerable(Of Char)
For Each c In w
If Char.IsUpper(c) Then Yield c
Next
End Function
Console.WriteLine(String.Join("", words("VisualBasicNet")))
' Inside LINQ: SelectMany with an iterator lambda.
Dim pairs = Enumerable.Range(1, 3).SelectMany(
Iterator Function(n As Integer) As IEnumerable(Of String)
For m = n + 1 To 3
Yield n & "-" & m
Next
End Function)
Console.WriteLine(String.Join(" ", pairs))
End Sub
End Module
3 2 1
1 2 3 4
VBN
1-2 1-3 2-3
Source: Iterators (Visual Basic)
The single-line attempt that does not compile:
Option Strict On
Module Program
Sub Main()
Dim numbers = Iterator Function() As IEnumerable(Of Integer) Yield 5
End Sub
End Module
Program.vb(5,23): error BC36674: Multiline lambda expression is missing 'End Function'.
Program.vb(5,70): error BC30205: End of statement expected.
Source: Iterator
The second lambda in the example captured limit as a variable, not as a value. It printed 1 2 3 4 because limit was changed
to 4 before the first MoveNext, and the iterator read limit when its loop started. This is the same closure rule as for
every lambda (see the delegates page), compounded by the deferred execution of iterators: a captured variable that changes
before the sequence is enumerated changes the sequence.
Consuming IAsyncEnumerable(Of T) from Visual Basic
Asynchronous streams (IAsyncEnumerable(Of T), IAsyncEnumerator(Of T) and IAsyncDisposable) exist in .NET since .NET Core
3.0, and the base class library returns them from APIs such as ChannelReader(Of T).ReadAllAsync, HttpContent.ReadFromJsonAsAsyncEnumerable
and the Entity Framework Core AsAsyncEnumerable. Visual Basic supports none of the language features built on them:
-
There is no
Await For Each.For Eachon anIAsyncEnumerable(Of T)is BC32023 ("Expression is of type 'IAsyncEnumerable(Of Integer)', which is not a collection type"), because the interface is notIEnumerable. -
There is no
Await Using.Usingcalls the synchronousDispose, andIAsyncDisposabledoes not have one. -
There are no asynchronous iterators.
Async Iterator Function ... As IAsyncEnumerable(Of T)is BC36936, and anAsyncfunction whose return type is notTask,Task(Of T)orSubis BC36945, includingValueTask(Of T). -
Awaitis not allowed in aCatchorFinallyblock (BC36943), unlike C#. That is the obstacle for the obvious hand-written translation ofawait foreach, which would putAwait enumerator.DisposeAsync()in aFinally.
Option Strict On
Module Program
Private Async Function CleanUpAsync(resource As IAsyncDisposable) As Task
Try
Await Task.Delay(1)
Finally
Await resource.DisposeAsync()
End Try
End Function
Sub Main()
End Sub
End Module
Program.vb(8,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.
Source: Await Operator
Option Strict On
Module Program
Private Async Function NextAsync() As ValueTask(Of Boolean)
Await Task.Delay(1)
Return True
End Function
Sub Main()
End Sub
End Module
Program.vb(4,43): error BC36945: The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).
Source: Async
What works is the explicit protocol: get the enumerator, loop on Await enumerator.MoveNextAsync(), remember any exception in a
Catch, dispose after the Try statement, and rethrow with ExceptionDispatchInfo so that the original stack trace
survives.
Option Strict On
Imports System.Runtime.ExceptionServices
Imports System.Threading
Imports System.Threading.Channels
Module Program
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
' VB has no "Await For Each", and Await is not allowed in Catch or Finally,
' so the loop that C# writes as "await foreach" is written out by hand:
' remember the exception, dispose asynchronously after the Try, then rethrow.
Private Async Function ConsumeAsync(source As IAsyncEnumerable(Of String)) As Task(Of Integer)
Dim count = 0
Dim failure As Exception = Nothing
Dim enumerator = source.GetAsyncEnumerator(CancellationToken.None)
Try
While Await enumerator.MoveNextAsync()
Console.WriteLine("received " & enumerator.Current)
count += 1
End While
Catch ex As Exception
failure = ex
End Try
Await enumerator.DisposeAsync()
If failure IsNot Nothing Then ExceptionDispatchInfo.Capture(failure).Throw()
Return count
End Function
Private Async Function RunAsync() As Task
Dim channel As Channel(Of String) = Channels.Channel.CreateUnbounded(Of String)()
Dim producer = Task.Run(
Async Function() As Task
For Each title In {"Compilers", "Refactoring", "Domain-Driven Design"}
Await channel.Writer.WriteAsync(title)
Await Task.Delay(10)
Next
channel.Writer.Complete()
End Function)
Dim total = Await ConsumeAsync(channel.Reader.ReadAllAsync())
Await producer
Console.WriteLine("total: " & total)
End Function
End Module
received Compilers
received Refactoring
received Domain-Driven Design
total: 3
Source: ChannelReader<T>.ReadAllAsync
The Channel producer is a Task.Run with an Async Function lambda that writes three titles and completes the writer;
ReadAllAsync() is the IAsyncEnumerable(Of String) that ConsumeAsync reads. Notice Channels.Channel.CreateUnbounded(Of String)()
with its namespace written out: the local variable is called channel, and since names are not case sensitive it would hide
the Channel class if the call were written without the namespace.
If you consume many streams, wrap the protocol in a generic extension method. The next example also shows the other
direction (producing a stream) the only way Visual Basic can: implementing IAsyncEnumerable(Of T) and
IAsyncEnumerator(Of T) by hand, with MoveNextAsync returning ValueTask(Of Boolean) built from a private Async Function
that returns Task(Of Boolean). It also shows the query operators of System.Linq.AsyncEnumerable, which are part of
.NET 10 and work on any IAsyncEnumerable(Of T). Two traps in the implementation: the interface method has an optional
CancellationToken parameter that the Implements method must repeat with Optional (without it BC30401 and BC30149 are
reported), and the Async modifier cannot be put on the ValueTask(Of Boolean) method.
Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Runtime.ExceptionServices
Imports System.Threading
Public Module AsyncSequenceExtensions
' A reusable stand-in for "Await For Each": run an action for every element.
<Extension>
Public Async Function ForEachAsync(Of T)(source As IAsyncEnumerable(Of T), action As Action(Of T)) As Task
Dim failure As Exception = Nothing
Dim enumerator = source.GetAsyncEnumerator(CancellationToken.None)
Try
While Await enumerator.MoveNextAsync()
action(enumerator.Current)
End While
Catch ex As Exception
failure = ex
End Try
Await enumerator.DisposeAsync()
If failure IsNot Nothing Then ExceptionDispatchInfo.Capture(failure).Throw()
End Function
End Module
' VB cannot write an asynchronous iterator, but it can implement the interfaces by hand.
Public Class CountdownStream
Implements IAsyncEnumerable(Of Integer)
Private ReadOnly _from As Integer
Public Sub New(from As Integer)
_from = from
End Sub
Public Function GetAsyncEnumerator(Optional cancellationToken As CancellationToken = Nothing) As IAsyncEnumerator(Of Integer) _
Implements IAsyncEnumerable(Of Integer).GetAsyncEnumerator
Return New Enumerator(_from)
End Function
Private NotInheritable Class Enumerator
Implements IAsyncEnumerator(Of Integer)
Private _next As Integer
Public Sub New(from As Integer)
_next = from
Current = 0
End Sub
Public Property Current As Integer Implements IAsyncEnumerator(Of Integer).Current
' Async cannot return ValueTask(Of T) in Visual Basic: wrap a Task(Of T) instead.
Public Function MoveNextAsync() As ValueTask(Of Boolean) Implements IAsyncEnumerator(Of Integer).MoveNextAsync
Return New ValueTask(Of Boolean)(AdvanceAsync())
End Function
Private Async Function AdvanceAsync() As Task(Of Boolean)
Await Task.Delay(1)
If _next < 0 Then Throw New InvalidOperationException("below zero")
If _next = 0 Then Return False
Current = _next
_next -= 1
Return True
End Function
Public Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
Console.WriteLine(" async enumerator disposed")
Return ValueTask.CompletedTask
End Function
End Class
End Class
Module Program
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
Private Async Function RunAsync() As Task
Await New CountdownStream(3).ForEachAsync(Sub(n) Console.WriteLine("tick " & n))
' System.Linq.AsyncEnumerable (part of .NET 10) supplies the query operators.
Dim evens = Await New CountdownStream(6).Where(Function(n) n Mod 2 = 0).ToListAsync()
Console.WriteLine("evens: " & String.Join(",", evens))
' The exception is rethrown after the asynchronous dispose.
Try
Await New CountdownStream(-1).ForEachAsync(Sub(n) Console.WriteLine("never " & n))
Catch ex As InvalidOperationException
Console.WriteLine("caught: " & ex.Message)
End Try
End Function
End Module
tick 3
tick 2
tick 1
async enumerator disposed
async enumerator disposed
evens: 6,4,2
async enumerator disposed
caught: below zero
Source: IAsyncEnumerable<T> Interface
Hand-written asynchronous enumerators are boilerplate that C# generates for you. When a library needs to produce asynchronous
streams, write that piece in C# (an async IAsyncEnumerable<T> method with yield return) in a separate project and
consume it from Visual Basic as shown; see
Interop with C# and Other .NET Languages.
For work that does not need streaming, Task(Of IEnumerable(Of T)), a Channel(Of T) or an event are simpler.
See Async and Await for the rest of the asynchronous model.
Performance and threading notes
-
An iterator allocates its state machine object on the first call and again for each enumerator after the first, which is cheap but not free. An iterator over an array in a hot loop is slower than a
Forloop over the array; use the iterator when the laziness, early exit or composition is worth it. -
The hidden class holds the parameters and locals as fields, so a local that references a large object keeps it alive until the enumeration ends or the enumerator is collected.
-
Iterator enumerators are not thread safe. Two threads that call
MoveNexton the same enumerator race on its state, and the iterator function itself can be enumerated from several threads only if each thread callsGetEnumeratoron its own and the body is safe to run concurrently. -
SyncLockcan contain aYield, and the lock is then held for as long as the consumer pauses the sequence between two elements, which is rarely what you want. Take the lock around a copy of the data and yield from the copy. -
Trimming and native AOT: an iterator compiles to an ordinary class and does not use reflection at run time, so it needs no trimming or AOT annotations.
Visual Basic and C# side by side
| Topic | Visual Basic | C# |
|---|---|---|
Declaring an iterator |
|
any method returning |
Produce a value |
|
|
End the sequence |
|
|
Iterator property |
|
a |
Iterator lambda |
|
not allowed; use a local function |
Local helper for eager validation |
a second private function |
a local function inside the method |
|
allowed |
compile error (CS1626) |
|
BC36939 |
compile error |
Re-yield a nested sequence |
loop over it with |
loop over it with |
Asynchronous iterators |
not available |
|
Consume an asynchronous stream |
|
|
|
BC36943 |
allowed |
Iterator in |
works on a copy of |
works on a copy of |
Older material you may meet
-
Before Visual Studio 2012 (Visual Basic 10 and earlier) there were no iterators. Tutorials from that period make a class enumerable with a hand-written
IEnumeratorclass holding an index, aMoveNextmethod that increments it, aCurrentproperty and an unusableReset. That code still compiles, andIteratorreplaces all of it with one function. -
Visual Basic 6 made a class usable in
For Eachwith a hiddenNewEnummember ([_NewEnum], flagged with the procedure ID-4) that returned anIUnknownfor anIEnumVARIANT. In .NET the same job isGetEnumerator. See Migrating Legacy Visual Basic. -
Books and articles written for .NET Framework or for Visual Basic before 2012 present
Yieldas a new feature or omit it. It is stable, is not tied to aLangVersion, and has gained no syntax since; the only compiler change since is the 17.13 fix forDisposedescribed above. The Learn page for theIteratormodifier was last updated in 2021; its examples useByVal(which is the default and is not written in new code) andConsole.ReadKey(which is not needed in a console project).
Pitfalls
| Pitfall | What to do |
|---|---|
Argument checks in an iterator run late |
Validate in an ordinary function and return a private iterator |
The iterator re-runs for every enumeration |
|
Possible multiple enumeration |
Do not call |
A |
Dispose manually driven enumerators with |
The loop variable is named like a property of the class |
Use a different name (BC30039) |
|
BC36939; hold the value in a variable and yield after the |
An infinite iterator hangs a |
Always consume it through |
Captured variable changes before the loop |
Copy it to a local inside the function that creates the iterator |
|
Use the explicit protocol shown above, or write the producer in C# |
Mixing |
Iterate over a copy ( |
References
-
IAsyncEnumerable<T>, IAsyncEnumerator<T>, IAsyncDisposable and System.Linq.AsyncEnumerable
-
ChannelReader<T>.ReadAllAsync, ValueTask<TResult> and ExceptionDispatchInfo
-
Await Operator and Async
-
yield statement (C# comparison)
-
Roslyn: Visual Basic compiler breaking changes after .NET 9 through .NET 10 (the 17.13 enumerator disposal change)