📜 ⬆️ ⬇️

Disposable ref structs in C # 8.0


Let's see what it says on the blog about upcoming changes in C # 8.0 (version of Visual Studio 2019 Preview 2):


"Stack-only structures appeared in C # 7.2. They are extremely useful, but their use is closely related to restrictions, such as the inability to implement interfaces. Now reference structures can be cleared using the Dispose method inside them without using the IDisposable interface. ”


So it is: stack-only ref structures do not implement interfaces, otherwise there would be a likelihood of their packing. Therefore, they cannot implement IDisposable, and we cannot use these structures in a using statement:


class Program { static void Main(string[] args) { using (var book = new Book()) { Console.WriteLine("Hello World!"); } } } ref struct Book : IDisposable { public void Dispose() { } } 

Attempting to run this code will result in a compilation error. :


 Error CS8343 'Book': ref structs cannot implement interfaces 

However, now, if we add the public Dispose method to the reference structure, the using statement will magically accept it, and everything will compile:


 class Program { static void Main(string[] args) { using (var book = new Book()) { // ... } } } ref struct Book { public void Dispose() { } } 

Moreover, thanks to the changes in the statement itself, you can now use using in a shorter form (the so-called using declarations):


 class Program { static void Main(string[] args) { using var book = new Book(); // ... } } 

But why?


This is a long story, but in general, explicit cleaning (deterministic finalization) is preferable to implicit (non-deterministic finalization). This is understandable on an intuitive level. It is better to explicitly clear resources as soon as possible (by calling Close, Dispose, or using statement), instead of waiting for an implicit cleanup that will happen “someday” (when the environment itself starts finalizers).


Therefore, when creating a type that owns a certain resource, it is better to provide for the possibility of cleaning explicitly. In C #, this is obviously the IDisposable interface and its Dispose method.


Note. Keep in mind that in the case of reference structures, only explicit cleaning is used, since the definition of finalizers is not possible for them.


Let's look at an illustrative example of a regular “unmanaged memory pool wrapper”. It occupies the smallest possible place (the heap is not used at all) precisely because of the reference structure intended for people obsessed with performance:


 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 } } 

Since the wrapper contains an unmanaged resource, we use the Dispose method for cleaning after use. Thus, the example looks something like this:


 static void Main(string[] args) { var array = new UnmanagedArray<int>(10); Console.WriteLine(array[0]); array.Dispose(); } 

This is inconvenient because you need to remember to call Dispose. This is also a painful decision, since exception handling is not appropriate here. Therefore, in order for Dispose to be called from within, a using statement was introduced. However, earlier, as already mentioned, it was impossible to apply it in this situation.


But in C # 8.0, you can use the advantages of the using operator in full:


 static void Main(string[] args) { using (var array = new UnmanagedArray<int>(10)) { Console.WriteLine(array[0]); } } 

At the same time, the code became more concise due to the announcements:


 static void Main(string[] args) { using var array = new UnmanagedArray<int>(10); Console.WriteLine(array[0]); } 

Two other examples below (much of the code omitted for brevity) are from the CoreFX repository.


The first example is the ValueUtf8Converter reference structure, which wraps a byte [] array from an array pool:


 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, which wraps two ValueListBuilder reference structures that need to be explicitly cleared (since they also manage arrays from an array pool):


 internal ref struct RegexWriter { ... private ValueListBuilder<int> _emitted; private ValueListBuilder<int> _intStack; ... public void Dispose() { _emitted.Dispose(); _intStack.Dispose(); } } 

Conclusion


Removable reference structures can be viewed as sparse types, which have a REAL destructor, as in C ++. It will be enabled as soon as the corresponding instance goes beyond the scope of the using operator (or the scope in the case of using declarations).


Of course, they will not suddenly become popular when writing ordinary, commercial-oriented programs, but if you create high-performance, low-level code, you should be aware of them.


We also have an article about our conference:


')

Source: https://habr.com/ru/post/447014/


All Articles