Compilation of different programming
projects I amuse myself with.
Sunday, 16 June 2024
Adding synchronous and asynchronous foreach support to Range in C#
In C# 8, we got Range and Index. This allows you to construct a sequence of values.
This article will look at diferent ways of adding foreach support in C# to Range.
Let's first look at enumeration in the synchronous case. This requires us to add an enumerator to Range as an extension method.
We define the following extension method GetEnumerator on Range to achieve this:
This requires us to create a custom enumerator class that has these methods :
MoveNext
Current
The rest of the custom enumerator is happening via the logic in these methods and the constructor of this custom enumerator.
The following code tests out this range enumeration:
Console.WriteLine($"Iterating range 10..1");
foreach (var num in (10..1))
{
Console.WriteLine(num);
}
Console.WriteLine($"Iterating range 20..30");
foreach (var num in20..30)
{
Console.WriteLine(num);
}
And we get the following output, showing we can enumerate a range both increasing and decreasing :
Let's look next how we can implement async support for iterations. I first started with having a go with IAsyncEnumerator. But I could not make the cancellation token work, since I could not pass it in. Instead I landed on a method GetItemsAsync that supports cancellation tokens being passed in and will respect cancellation. Here is the extension methods I created :
publicstaticclassRangeExtensions
{
publicstaticasync IAsyncEnumerable<int> GetItemsAsync(this Range range, CancellationToken cancellationToken = default)
{
awaitforeach (var num in range)
{
if (cancellationToken.IsCancellationRequested){
Console.WriteLine($"Iteration FAILED: Exiting the IAsyncEnumerable iteration as soon as possible since cancellation was requested at index: {num}");
}
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100);
yieldreturn num;
}
}
// Existing asynchronous enumeration with delay and cancellation tokenpublicstatic CustomIntAsyncEnumerator GetAsyncEnumerator(this Range range)
=> new CustomIntAsyncEnumerator(range);
publicclassCustomIntAsyncEnumerator : IAsyncEnumerator<int>
{
privateint _current;
privatereadonlyint _start;
privatereadonlyint _end;
privatereadonlybool _isDescending;
publicCustomIntAsyncEnumerator(Range range)
{
_start = range.Start.Value;
_end = range.End.Value;
_isDescending = _start > _end;
_current = _isDescending ? _start + 1 : _start - 1;
}
publicint Current => _current;
publicasync ValueTask<bool> MoveNextAsync()
{
// Wait for the specified delay timeawait Task.Delay(0);
if (_isDescending)
{
if (_current > _end)
{
_current--;
returntrue;
}
}
else
{
if (_current < _end)
{
_current++;
returntrue;
}
}
returnfalse;
}
public ValueTask DisposeAsync()
{
// Perform any necessary cleanup herereturnnew ValueTask();
}
}
}
The code below tests the async enumeration. The benefit of the IAsyncEnumerator is that you can iterate without any hassles with
cancellation token, but you should respect cancellation tokens. So the last method call calling the method GetItemsAsync
do respect the cancellation token and also accepts such a token being passed in. I tested out the code in this article with Linqpad 7, and developed it using this small IDE.
var rangeOneHundredDownTo90 = 100..90;
Console.WriteLine($"Await foreach over variable {nameof(rangeOneHundredDownTo90)}");
awaitforeach (var num in rangeOneHundredDownTo90)
{
Console.WriteLine(num);
}
Console.WriteLine("Async iteration from 140 to 180, waiting 500 ms between each iteration");
var range = 140..180;
var cts = new CancellationTokenSource();
var token = cts.Token;
awaitforeach (var num in range.GetItemsAsync(token))
{
if (num >= 170)
{
cts.Cancel();
}
Console.WriteLine(num);
}
The screen shot below shows that the cancellation token was throwing a OperationCanceledException as was the intent of the demo code.
Code was written in Linqpad 7, so I used .NET 7 for the code above. You must have at least C# 8 language version in your framework to make it work, possible C# 9 language since we add custom enumeration here with async.
Code was written in Linqpad 7, so I used .NET 7 for the code above. You must have at least C# 8 language version in your framework to make it work, possible C# 9 language since we add custom enumeration here with async.
ReplyDelete