Go (https://golang.org) has a really nice little language feature called defer
, which is a keyword that lets you defer a statement until the current function returns, and you can see an example here. Given all the new language features in C# 8.0, I wanted to see what it would look like to use this in C# today.
The Problem
So I have the following code:
static void MyMethod()
{
var buffer = ArrayPool<byte>.Shared.Rent(81920);
try
{
// do some stuff with buffer
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
In order to avoid a memory leak, we must return the buffer
to the pool, but to ensure this happens under all possible circumstances we have to use a try
and finally
block, and this adds some indentation, and noise that we don't really want.
An appropriate solution to this particular problem today would be to wrap the rented buffer in an object that represents the resource and have it implement IDisposable
. Using C# 8.0 new using
syntax we can do:
static void MyMethod()
{
using var buffer = new PooledMemory(81920);
// do some stuff with buffer
}
Now the Dispose
method on PooledMemory
will be called implicitly at the end of the method block just as if we used a try
/finally
block, and I have less indentation and fewer curly braces.
However imagine if we had some cleanup task, that didn't really warrant encapsulating in a type, so random cleanup, diagnostic logging, whatever. We don't have a simple way to do this in the language.
Building a Defer Method
Let's start by having an IDisposable
struct that takes an Action
:
static DeferDisposable Defer(Action action) => new DeferDisposable(action);
internal readonly struct DeferDisposable : IDisposable
{
readonly Action _action;
public DeferDisposable(Action action) => _action = action;
public void Dispose() => _action();
}
I have chosen to use a struct
here so that there's just a little less heap allocation, as it turns out that using
will not box a struct when calling Dispose
, see here.
Now let's go back to the calling code:
static void MyMethod()
{
var buffer = ArrayPool<byte>.Shared.Rent(81920);
using var _ = Defer(() => ArrayPool<byte>.Shared.Return(buffer));
// do some stuff with buffer
}
We are using the discard feature here (var _
) too as we don't care about the value, we just want it for the purpose of applying using
. Edit: As pointed out in the comments, this isn't a discard, but rather a variable declaration of the variable _
.
Reducing Allocations
The above solution works and is fine, however we are capturing the buffer
variable here within the lambda, this results in the compiler allocating a Action
instance for every call, if we can avoid any references outside of the lambda then the compiler can avoid this and only create the Action
once for the entire lifetime of application.
We can avoid capturing variables by creating overloads for the N parameters we need to reference within the lambda.
static DeferDisposable<T> Defer<T>(Action<T> action, T param1) =>
new DeferDisposable<T>(action, param1);
internal readonly struct DeferDisposable<T1> : IDisposable
{
readonly Action<T1> _action;
readonly T1 _param1;
public DeferDisposable(Action<T1> action, T1 param1) => (_action, _param1) = (action, param1);
public void Dispose() => _action.Invoke(_param1);
}
Unfortunately there is no way to explicitly ensure no variable capture happens, there is no static
keyword for lambdas yet. In the calling code, you'll have to be careful not to accidentally capture variables from the outer scope.
Here is the usage:
static void MyMethod()
{
var buffer = ArrayPool<byte>.Shared.Rent(81920);
using var _ = Defer(b => ArrayPool<byte>.Shared.Return(b), buffer);
// do some stuff with buffer
}
Wrapping up
So it turns out we can do this with C# 8.0, however it's not particularly concise or idiomatic. If we want it to have practically no performance cost as well, it's even less concise. So I don't recommend trying this out in your code, this was just a thought experiment.
There is a language proposal for proper support of this in C# here, and it could make the C# 9 timeframe. So if this interests you, be sure to subscribe to that issue 👀.
Comments