This month, I will show you how to use the Yield operator. This operator was first introduced in C# and later ported into VB.Net after polyglot (speaking both C# and VB) developers requested it from Microsoft.
There are still many developers thinking that both languages are not on par for this specific feature, but they really are.
Available source code
Both VB and C# versions are provided this month.
The solution was created using Visual Studio 2017 but should also work in previous versions. The Yield keyword was added to the .Net Framework 2.0.
What is it?
The Yield keyword is used in iterators to return one item.
An Iterator steps through a collection. As we will soon see in the examples, an Iterator is defined as a read only property that returns an IEnumerable (or similar type). In VB.Net, we need to add the Iterator modifier to the property definition.
When the Yield keyword is reached within the loop of an iterator, the current item of the collection is returned to the caller method. When the loop of the caller method is ready for the next item, the code from the iterator method continues where it was and eventually return another item.
But this is not multi-tasking nor multi-threading. The code runs synchronously. It just pauses the execution of the iterator method until the next item is requested.
Building the UI
To demonstrate how we can benefit from the Yield keyword, I have created a very simple UI with 4 buttons and a multi-lines textbox (all having default names).
Figure 1: the demo application in action
A first test – returning items
The first example is to first introduce the concepts using a very simple data set built on the fly.
As you can see here, we have defined an iterator method (named NextDalton1). To keep it to its simplest form, this method does not have any loop. Instead, it is composed of 5 Yield statements.
The click event handler of Button1 is using a For Each statement to loop through the results of the NextDalton1 method. But as previously explained, instead of executing all the code from this method to return a full list, NextDalton1 will run until the first Yield keyword is met. At this point, an item containing Joe Dalton will be returned. Not a list, just one cCharacter instance. The loop of the event will then print the values to the textbox and continue to the next item. At this point, the execution of NextDalton1 will continue exactly where it previously stopped returning a second item and so on until the iterator method has completed.
To fully understand the what I mean, you should step through this code line by line. You will discover exactly what I mean by “continue exactly where it previously stopped returning a second item”.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click TextBox1.Clear() For Each dalton In NextDalton1 TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}") TextBox1.AppendText(Environment.NewLine) Next End Sub Private ReadOnly Iterator Property NextDalton1 As IEnumerable(Of cCharacter) Get Yield New cCharacter With {.FirstName = "Joe", .LastName = "Dalton"} Yield New cCharacter With {.FirstName = "Jack", .LastName = "Dalton"} Yield New cCharacter With {.FirstName = "William", .LastName = "Dalton"} Yield New cCharacter With {.FirstName = "Averell", .LastName = "Dalton"} Yield New cCharacter With {.FirstName = "Ma", .LastName = "Dalton"} End Get End Property
private void Button1_Click(object sender, EventArgs e) { TextBox1.Clear(); foreach (cCharacter dalton in NextDalton1) { TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}"); TextBox1.AppendText(Environment.NewLine); } } private IEnumerable<cCharacter> NextDalton1 { get { yield return new cCharacter() { FirstName = "Joe", LastName = "Dalton" }; yield return new cCharacter() { FirstName = "Jack", LastName = "Dalton" }; yield return new cCharacter() { FirstName = "William", LastName = "Dalton" }; yield return new cCharacter() { FirstName = "Averell", LastName = "Dalton" }; yield return new cCharacter() { FirstName = "Ma", LastName = "Dalton" }; } }
A second test – looping a collection
It doesn’t happen very often that we can define all the items as we see in the previous example.
This second example defined an iterator property (named NextDalton2) that loops through a collection. After a few iteration (intI > 2), the execution breaks preventing the full collection from being processed all 5 items being returned.
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click TextBox1.Clear() For Each dalton In NextDalton2 TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}") TextBox1.AppendText(Environment.NewLine) Next End Sub Private ReadOnly Iterator Property NextDalton2 As IEnumerable(Of cCharacter) Get Dim intI As Integer = 0 For Each character As cCharacter In mlstCharacters Yield character intI += 1 If intI > 2 Then Exit For Next End Get End Property
private void Button2_Click(object sender, EventArgs e) { TextBox1.Clear(); foreach (cCharacter dalton in NextDalton2) { TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}"); TextBox1.AppendText(Environment.NewLine); } } private IEnumerable<cCharacter> NextDalton2 { get { int intI = 0; foreach (cCharacter character in _lstCharacters) { yield return character; intI += 1; if (intI > 2) yield break; } } }
And a third test – filtering the collection
Somewhat like the previous example, this third one will filter the collection for item having the first name starting with a specific letter directly in the iterator again preventing the full collection to be returned.
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click TextBox1.Clear() For Each dalton In NextDalton3 TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}") TextBox1.AppendText(Environment.NewLine) Next End Sub Private ReadOnly Iterator Property NextDalton3 As IEnumerable(Of cCharacter) Get For Each character As cCharacter In mlstCharacters If character.FirstName.StartsWith("J") Then Yield character End If Next End Get End Property
private void Button3_Click(object sender, EventArgs e) { TextBox1.Clear(); foreach (cCharacter dalton in NextDalton3) { TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}"); TextBox1.AppendText(Environment.NewLine); } } private IEnumerable<cCharacter> NextDalton3 { get { foreach (cCharacter character in _lstCharacters) { if (character.FirstName.StartsWith("J")) yield return character; } } }
A last test – using LINQ
But I think the real power of iterators will be justified here.
Because the result of an iterator is an IEnumerator, it can very easily be used within a LINQ query and the various operators that limits the number of items processed. You can think here of Take and First for example (just to name 2). Instead of having LINQ to work on the full collection, it will query the iterator until the condition is met.
For example, in this example, because “Take(2)” is used, the NextDalton1 property will be called twice and only 2 items will be returned instead of the full collection.
Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click TextBox1.Clear() For Each dalton In NextDalton1.Take(2) TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}") TextBox1.AppendText(Environment.NewLine) Next End Sub
private void Button4_Click(object sender, EventArgs e) { TextBox1.Clear(); foreach (cCharacter dalton in NextDalton1.Take(2)) { TextBox1.AppendText($"{dalton.FirstName} {dalton.LastName}"); TextBox1.AppendText(Environment.NewLine); } }
What are the benefits?
If you have a large collection of items and you know in advance that not all items will be processed, you might want to use the Yield keyword to return items from the collection to the caller method only as requested.
That might also help you solving memory issues. If your collection is fully filled with a lot of very large objects, by returning items one-by-one can save your life because objects will be loaded only one at a time.
It can also help you improve performance if your collection needs to be filled with items that takes a lot of time to initialize and you don’t use them all. Why initialize a collection of 1000 items resulting from a process taking half a second to initialize when you might only take the top 10 items?
If you end up using the full collection of items and that collection is not too large or too long to populate, might as well just return the list from the class and forget about iterators.
Conclusion
This is an old construct introduced in the .Net Framework 2.0 that is very useful but not used as it should. It can also helps lower the memory usage and improve performance. Why not give it a try?