Recently I was looking at the source code of Stream.CopyToAsync
, which is available here on github.
The author first validates the parameters and then calls a static
local function that does the actual processing. I find static
local functions to be quite useful when I need to reuse a functionality, but don’t wan’t to add yet another private method.
But in Stream.CopyToAsync
the static local function is called just once at the very end of the declaring method. I don’t see any benefit here.
=> Is there any objective benefit (i.e. performance, security, better compiler optimizations, memory usage, …) to use a static local function here?
Please note that I’m not asking about readability, because that’s somewhat opinionated.
Code:
public virtual Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
ValidateCopyToArguments(destination, bufferSize);
if (!CanRead)
{
if (CanWrite)
{
ThrowHelper.ThrowNotSupportedException_UnreadableStream();
}
ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name);
}
return Core(this, destination, bufferSize, cancellationToken);
static async Task Core(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int bytesRead;
while ((bytesRead = await source.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
Which is in my opinion exactly the same as without a static local function:
public virtual async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
ValidateCopyToArguments(destination, bufferSize);
if (!CanRead)
{
if (CanWrite)
{
ThrowHelper.ThrowNotSupportedException_UnreadableStream();
}
ThrowHelper.ThrowObjectDisposedException_StreamClosed(GetType().Name);
}
// return Core(this, destination, bufferSize, cancellationToken);
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int bytesRead;
while ((bytesRead = await this.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
3
Sweeper gave a good explanation in the comments, which I will summarize here.
Also this question and this microsoft devblog post and especially this blog post helped me understand the great idea behind this.
Key points:
- The static local function
Core
encapsulates allasync
operations ofStream.CopyToAsync
-> the compiler will transformCore
to an async state machine. Stream.CopyToAsync
itself is notasync
and will not be transformed to a state machine.- If an exception is thrown during validation (befor the call to
Core
) the exception will directly be passed on to the caller. - Exceptions thrown in
Core
will only be passed to the caller when the resultingTask
is awaited.
Minmalistic example, as of my understanding:
Task Foo(Argument arg)
{
if(!arg.IsValid) throw new ArgumentException();
return Core(arg);
static async Task Core(Argument arg) { await SomeAsyncOperations(); }
}
async Task Bar(Argument arg)
{
if(!arg.IsValid) throw new ArgumentException();
await SomeAsyncOperations();
}
// no difference in usage when awaited immediately
await Foo(invalidArg); // throws here
await Bar(invalidArg); // throws here
// big difference if awaited somewhere else (or not awaited at all)
var fooTask = Foo(invalidArg); // throws here (!)
await fooTask;
var barTask = Bar(invalidArg); // no throw here (!)
await barTask; // throws here