I have a method LoopAsync
that takes a lambda parameter, and invokes this lambda repeatedly a number of times with a delay. Both the method and the lambda are asynchronous:
static async Task LoopAsync(Func<int, Task> action,
int start, int count, int delayMsec)
{
for (int i = start; i < start + count; i++)
{
await Task.Delay(delayMsec).ConfigureAwait(false);
await action(i).ConfigureAwait(false);
}
}
Now I want to enhance the LoopAsync
with an overload that takes a synchronous lambda parameter (an Action<int>
). I want to avoid duplicating my code, so I am thinking to implement the overload using the existing implementation like this:
static Task LoopAsync(Action<int> action,
int start, int count, int delayMsec)
{
return LoopAsync(i =>
{
action(i); return Task.CompletedTask;
}, start, count, delayMsec);
}
What I dislike with this approach is that it captures the action
argument, resulting in an allocation of an object every time the LoopAsync
is invoked. I want to prevent this capturing from happening. Is it possible?
To summarize, I want to have two method overloads, the first with asynchronous lambda and the second with synchronous lambda, that are sharing the same implementation, without incurring the penalty of allocating closure objects on the heap. I am equally OK with the one overload being based on the other, or with both being based on the same private
core implementation.
This question was inspired by a recent question by Avrohom Yisroel.
I think that I have found a solution to this problem. I made a private LoopCoreAsync
method that takes an additional generic TArg
argument:
private static async Task LoopCoreAsync<TArg>(Func<TArg, int, Task> action,
TArg arg, int start, int count, int delayMsec)
{
for (int i = start; i < start + count; i++)
{
await Task.Delay(delayMsec).ConfigureAwait(false);
await action(arg, i).ConfigureAwait(false);
}
}
...and then I used this method for implementing the two LoopAsync
overloads:
static Task LoopAsync(Func<int, Task> action,
int start, int count, int delayMsec)
{
return LoopCoreAsync(static (arg, i) =>
{
return arg(i);
}, action, start, count, delayMsec);
}
static Task LoopAsync(Action<int> action,
int start, int count, int delayMsec)
{
return LoopCoreAsync(static (arg, i) =>
{
arg(i); return Task.CompletedTask;
}, action, start, count, delayMsec);
}
The TArg
is resolved as Func<int, Task>
for the first overload, and as Action<int>
for the second overload. The static
lambda modifier (C# 9 feature) ensures that no variables are captured by the two lambdas.
According to my measurements, this optimization prevents the allocation of 88 bytes on the heap for each LoopAsync
invocation (.NET 6, Release built).