Page 46 - MSDN Magazine, November 2019
P. 46
C#
Iterating with Async
Enumerables in C# 8
Stephen Toub
Since the beginning of .NET, enumeration of collections has been the bread-and-butter of many programs. The non-generic System.Collections.IEnumerable interface enabled code to retrieve a System.Collections.IEnumerator, which in turn provided the basic functionality of MoveNext and Current for forward iterating through each element in the source collection. The C# language simplified this iteration further via the foreach keyword:
IEnumerable src = ...;
foreach (int item in src) Use(item);
When .NET Framework 2.0 came around with generics, System.Collections.Generic.IEnumerable<T> was introduced, enabling retrieving a System.Collections.Generic.IEnumerator<T> to support strongly typed iteration, a boon for both productivity and performance (due in large part to avoiding boxing):
IEnumerable<int> src = ...;
foreach (int item in src) Use(item);
As with the non-generic interface, this use of foreach is trans- formed by the compiler into calls on the underlying interfaces:
IEnumerable<int> src = ...; IEnumerator<int> e = src.GetEnumerator(); try
{
while (e.MoveNext()) Use(e.Current); finally { if (e != null) e.Dispose(); }
In addition, C# 2.0 introduced iterators, which make it simple for developers to use normal control flow constructs to author cus- tom enumerables, with the compiler rewriting developer methods
that employ “yield return” statements into state machines suitable to implement IEnumerable<T> and IEnumerator<T>:
static IEnumerable<int> Range(int start, int count) {
for (int i = 0; i < count; i++) yield return start + i;
}
...
foreach (int item in Range(10, 3))
Console.Write(item + " "); // prints 10 11 12
(This example shows using “yield return” to create an IEnumer- able<T>, but the language also supports using IEnumerator<T> instead, in which case it’s equivalent to an IEnumerable<T> being produced and then GetEnumerator being called on the result.)
Fast forward to .NET Framework 4, which introduced the System.Threading.Tasks.Task and Task<T> types, and .NET Frame- work 4.5 and C# 5, which introduced the async and await keywords to drastically simplify asynchronous programming. Instead of hav- ing to write complicated callback-based “spaghetti” code, developers could again use normal control flow constructs to author their asyn- chronous operations, with the compiler rewriting the developers’ async methods that employ await expressions into state machines that generate Tasks and internally use callbacks:
static async Task PrintAsync(string format, int iterations, int delayMilliseconds) {
for (int i = 0; i < iterations; i++) {
await Task.Delay(delayMilliseconds);
Console.WriteLine(string.Format(format, i)); }
}
...
await PrintAsync("Iteration {0}", 5, 1_000);
(Subsequent releases of C# and .NET have seen many improve- ments around this asynchronous support, including runtime and compiler enhancements, as well as library additions, such as the introduction of the ValueTask and ValueTask<T> types that enable using async and await with fewer allocations.)
With the compiler providing support for iterators and for async methods, a common question that’s asked addresses the combination of the two. You can use yield to author synchro- nous enumerables, and you can use async and await to author
}
This article discusses:
• New async interfaces
• Under the hood of async iterators
• Understanding “thread-safe” async enumerables Technologies discussed:
C# 8, .NET Core 3.0
Code download available at:
github.com/dotnet/reactive
34 msdn magazine