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 voidvoidSpecifies a return value type for a method that does not return a value. MyMethodvoid Example.MyMethod()(){ varbyte[] bufferbyte[]? buffer = ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Rentbyte[] ArrayPool<byte>.Rent(int minimumLength)Retrieves a buffer that is at least the requested length.ParametersminimumLength — The minimum length of the array.ReturnsAn array of type T that is at least minimumLength in length.(81920);
try { // do some stuff with buffer } finally { ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Returnvoid ArrayPool<byte>.Return(byte[] array, bool clearArray = false)Returns an array to the pool that was previously obtained using the Int32) method on the same ArrayPool`1 instance.Parametersarray — A buffer to return to the pool that was previously obtained using the Int32) method.clearArray — Indicates whether the contents of the buffer should be cleared before reuse. If clearArray is set to , and if the pool will store the buffer to enable subsequent reuse, the Boolean) method will clear the array of its contents so that a subsequent caller using the Int32) method will not see the content of the previous caller. If clearArray is set to or if the pool will release the buffer, the array's contents are left unchanged.(bufferbyte[]? 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 voidvoidSpecifies a return value type for a method that does not return a value. MyMethodvoid Example.MyMethod()(){ using varPooledMemory bufferPooledMemory? buffer = new PooledMemoryPooledMemory(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 DeferDisposableExample.DeferDisposable DeferExample.DeferDisposable Example.Defer(Action action)(ActionActionEncapsulates a method that has no parameters and does not return a value. actionAction action) => new DeferDisposableExample.DeferDisposable(actionAction action);
internal readonly struct DeferDisposableExample.DeferDisposable :Example.DeferDisposable IDisposableIDisposableProvides a mechanism for releasing unmanaged resources.{ readonly ActionActionEncapsulates a method that has no parameters and does not return a value. _actionAction Example.DeferDisposable._action; public DeferDisposableExample.DeferDisposable.DeferDisposable(Action action)(ActionActionEncapsulates a method that has no parameters and does not return a value. actionAction action) => _actionAction Example.DeferDisposable._action = actionAction action; public voidvoidSpecifies a return value type for a method that does not return a value. Disposevoid Example.DeferDisposable.Dispose()() => _actionAction Example.DeferDisposable._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 voidvoidSpecifies a return value type for a method that does not return a value. MyMethodvoid Example.MyMethod()(){ varbyte[] bufferbyte[]? buffer = ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Rentbyte[] ArrayPool<byte>.Rent(int minimumLength)Retrieves a buffer that is at least the requested length.ParametersminimumLength — The minimum length of the array.ReturnsAn array of type T that is at least minimumLength in length.(81920);
using varDeferDisposable _DeferDisposable _ = DeferDeferDisposable Example.Defer(Action action)(() => ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Returnvoid ArrayPool<byte>.Return(byte[] array, bool clearArray = false)Returns an array to the pool that was previously obtained using the Int32) method on the same ArrayPool`1 instance.Parametersarray — A buffer to return to the pool that was previously obtained using the Int32) method.clearArray — Indicates whether the contents of the buffer should be cleared before reuse. If clearArray is set to , and if the pool will store the buffer to enable subsequent reuse, the Boolean) method will clear the array of its contents so that a subsequent caller using the Int32) method will not see the content of the previous caller. If clearArray is set to or if the pool will release the buffer, the array's contents are left unchanged.(bufferbyte[]? 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 DeferDisposableExample.DeferDisposable<T><Example.DeferDisposable<T>TT>Example.DeferDisposable<T> DeferExample.DeferDisposable<T> Example.Defer<T>(Action<T> action, T param1)<Example.DeferDisposable<T> Example.Defer<T>(Action<T> action, T param1)TExample.DeferDisposable<T> Example.Defer<T>(Action<T> action, T param1)>Example.DeferDisposable<T> Example.Defer<T>(Action<T> action, T param1)(ActionAction<T>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.<Action<T>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.TT>Action<T>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates. actionAction<T> action, TT param1T param1) => new DeferDisposableExample.DeferDisposable<T><Example.DeferDisposable<T>TT>Example.DeferDisposable<T>(actionAction<T> action, param1T param1);
internal readonly struct DeferDisposableExample.DeferDisposable<T1><Example.DeferDisposable<T1>T1T1>Example.DeferDisposable<T1> :Example.DeferDisposable<T1> IDisposableIDisposableProvides a mechanism for releasing unmanaged resources.{ readonly ActionAction<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.<Action<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.T1T1>Action<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates. _actionAction<T1> Example.DeferDisposable<T1>._action; readonly T1T1 _param1T1 Example.DeferDisposable<T1>._param1; public DeferDisposableExample.DeferDisposable<T1>.DeferDisposable(Action<T1> action, T1 param1)(ActionAction<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.<Action<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates.T1T1>Action<T1>Encapsulates a method that has a single parameter and does not return a value.Parametersobj — The parameter of the method that this delegate encapsulates. actionAction<T1> action, T1T1 param1T1 param1) => (_actionAction<T1> Example.DeferDisposable<T1>._action, _param1T1 Example.DeferDisposable<T1>._param1) = (actionAction<T1> action, param1T1 param1); public voidvoidSpecifies a return value type for a method that does not return a value. Disposevoid Example.DeferDisposable<T1>.Dispose()() => _actionAction<T1> Example.DeferDisposable<T1>._action.Invokevoid Action<T1>.Invoke(T1 obj)(_param1T1 Example.DeferDisposable<T1>._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 voidvoidSpecifies a return value type for a method that does not return a value. MyMethodvoid Example.MyMethod()(){ varbyte[] bufferbyte[]? buffer = ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Rentbyte[] ArrayPool<byte>.Rent(int minimumLength)Retrieves a buffer that is at least the requested length.ParametersminimumLength — The minimum length of the array.ReturnsAn array of type T that is at least minimumLength in length.(81920);
using varDeferDisposable<byte[]> _DeferDisposable<byte[]> _ = DeferDeferDisposable<byte[]> Example.Defer<byte[]>(Action<byte[]> action, byte[] param1)(bbyte[] b => ArrayPoolArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].<ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[].bytebyteRepresents an 8-bit unsigned integer.>ArrayPool<byte>Provides a resource pool that enables reusing instances of type T[]..SharedArrayPool<byte> ArrayPool<byte>.SharedGets a shared ArrayPool`1 instance.ReturnsA shared ArrayPool`1 instance..Returnvoid ArrayPool<byte>.Return(byte[] array, bool clearArray = false)Returns an array to the pool that was previously obtained using the Int32) method on the same ArrayPool`1 instance.Parametersarray — A buffer to return to the pool that was previously obtained using the Int32) method.clearArray — Indicates whether the contents of the buffer should be cleared before reuse. If clearArray is set to , and if the pool will store the buffer to enable subsequent reuse, the Boolean) method will clear the array of its contents so that a subsequent caller using the Int32) method will not see the content of the previous caller. If clearArray is set to or if the pool will release the buffer, the array's contents are left unchanged.(bbyte[] b), bufferbyte[]? 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