Among many things that are coming with the upcoming C# 8.0, one perfectly fits the topic of ref structs I’ve raised in my previous post – disposable ref structs.
As one of the blog posts announcing C# 8.0 changes (in Visual Studio 2019 Preview 2) mentions:
“Ref structs were introduced in C# 7.2, and this is not the place to reiterate their usefulness, but in return they come with some severe limitations, such as not being able to implement interfaces. Ref structs can now be disposable without implementing the IDisposable interface, simply by having a Dispose method in them.”
Indeed, as we should remember from my previous post, ref structs cannot implement interface because it would expose them to boxing possibility. But because of that we cannot make them implementing IDisposable, and thus we cannot use them in using statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Program { static void Main(string[] args) { using (var book = new Book()) { Console.WriteLine("Hello World!"); } } } ref struct Book : IDisposable { public void Dispose() { } } |
The above code ends with compilation eerror
1 |
Error CS8343 'Book': ref structs cannot implement interfaces |
But from now on, if we add public Dispose method to our ref struct, it will be auto-magically consumed by the using statement and the whole thing compiles:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Program { static void Main(string[] args) { using (var book = new Book()) { // ... } } } ref struct Book { public void Dispose() { } } |
Even more, due to changes in the using statement itself (described in the mentioned blog post), we can now use more concise way of using… using (so-called using declarations):
1 2 3 4 5 6 7 8 |
class Program { static void Main(string[] args) { using var book = new Book(); // ... } } |
But… why?
This is a long topic but in general explicit cleanup (deterministic finalization) is preferred over implicit one (not-deterministic finalization). This is somehow intuitive. It is better to explicitly make a cleanup as soon as it is possible (by calling Close, Dispose, or using statement), instead of waiting for non-explicit cleanup that will occur “at some time” (by the runtime executing finalizers).
Thus, when designing type owning some resource, we would like to have some explicit cleanup possibility. In C# the choice is obvious – we have a well-known contract in the form of IDisposable interface and its Dispose method.
Note. Please note that in case of ref structs the cleanup choice is also limited to explicit cleanup as ref structs cannot have finalizers defined.
Let’s take an example of a trivial “unmanaged memory pool wrapper” presented below, for illustrative purposes. Dedicated to performance-freaks, it is a ref struct to make it as lightweight as possible (no heap utilization ever):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public unsafe ref struct UnmanagedArray<T> where T : unmanaged { private T* data; public UnmanagedArray(int length) { data = // get memory from some pool } public ref T this[int index] { get { return ref data[index]; } } public void Dispose() { // return memory to the pool } } |
As it owns unmanaged resource underneath, we introduced Dispose method to cleanup it at the end of usage. Thus, example usage could look like:
1 2 3 4 5 6 |
static void Main(string[] args) { var array = new UnmanagedArray<int>(10); Console.WriteLine(array[0]); array.Dispose(); } |
This is obviously cumbersome – we need to remember about calling Dispose. And obviously not-happy-path, like exception handling, is not properly handled here. This is why using statement was introduced, to make sure it will be called underneath. But as already said, till C# 8.0 it could not be used here.
But now in C# 8.0, without a problem, we can make use of all benefits from the using statement:
1 2 3 4 5 6 7 |
static void Main(string[] args) { using (var array = new UnmanagedArray<int>(10)) { Console.WriteLine(array[0]); } } |
Even more, thanks to using declarations, our code becomes more concise:
1 2 3 4 5 |
static void Main(string[] args) { using var array = new UnmanagedArray<int>(10); Console.WriteLine(array[0]); } |
Another two examples presented below (with a lot of code cut off for brevity) come from CoreFX repository.
The first example is ValueUtf8Converter ref struct that wraps around byte[] array from the array pool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
internal ref struct ValueUtf8Converter { private byte[] _arrayToReturnToPool; ... public ValueUtf8Converter(Span<byte> initialBuffer) { _arrayToReturnToPool = null; } public Span<byte> ConvertAndTerminateString(ReadOnlySpan<char> value) { ... } public void Dispose() { byte[] toReturn = _arrayToReturnToPool; if (toReturn != null) { _arrayToReturnToPool = null; ArrayPool<byte>.Shared.Return(toReturn); } } } |
The second example is RegexWriter that wraps two ValueListBuilder ref structs that need to be explicitly cleaned (as they also manage arrays from the array pool):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
internal ref struct RegexWriter { ... private ValueListBuilder<int> _emitted; private ValueListBuilder<int> _intStack; ... public void Dispose() { _emitted.Dispose(); _intStack.Dispose(); } } |
In summary
We can treat disposable ref structs as lightweight types that have A REAL destructor, known from C++. It will be executed as soon as the corresponding instance goes out of the scope of the underlying using statement (or enclosing scope in case of using declaration).
For sure it is not a feature that will suddenly become extremely popular in writing regular, business-driven code. But few layers below, when writing high-performant low-level code, it is worth to know about such possibility!
Great summary. Especially the using declarations seem to be very useful. Ref structs look awesome for tracing method/enter leave scenarios. I have an existing library with a normal struct but it could cause issues if I make it ref struct because I am not so sure how this will look to the managed C++ compiler which was not updated for ages.
Thanks! Oh yes, managed C++ compiler, there was one 🙂 Good question though.
But you can not prevent prevent s ValueUtf8Converter from being copied (for example by passing it to another function). The copy can then be Disposed()-d, and if the original then access its Span() you are in trouble…