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 👀.