Disposable ref structs in C # 8.0

Original author: Konrad Kokosa
  • Transfer

Let's see what the blog says about the 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 at the same time their use is closely related to restrictions, for example, 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 the probability of their packaging would arise. Therefore, they cannot implement IDisposable, and we cannot use these structures in the 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 a public method Disposeto the link structure, the operator will usingmagically 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 changes in the statement itself, you can now use using in a shorter form (the so-called declarations using):


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

But why?


This is a long story, but in general, explicit cleanup (deterministic finalization) is preferable to implicit (non-deterministic finalization). This is intuitive. It’s better to explicitly clear the resources as soon as possible (by calling Close, Dispose or using the statement), instead of waiting for the implicit cleaning that will happen “someday” (when the environment itself starts the 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 an interface IDisposableand its method Dispose.


Note. Remember that in the case of referenced structures, only explicit cleanup is used, since defining finalizers for them is not possible.


Let's look at an illustrative example of the usual “wrapper for an unmanaged memory pool”. It occupies the smallest possible space (the heap is not used at all) precisely thanks to the link structure intended for people obsessed with performance:


public unsafe ref struct UnmanagedArray 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 to clean it after use. So the example looks something like this:


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

This is inconvenient because you need to remember about calling Dispose. Also, this is a painful decision, since handling exceptions properly is not applicable here. Therefore, in order for Dispose to be called from within, the 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 take full advantage of the using statement:


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

At the same time, the code has become more concise thanks to the declarations:


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

The other two examples below (much of the code omitted for brevity) are taken 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 initialBuffer)
   {
      _arrayToReturnToPool = null;
   }
   public Span ConvertAndTerminateString(ReadOnlySpan value)
   {
      ...
   }
   public void Dispose()
   {
      byte[] toReturn = _arrayToReturnToPool;
      if (toReturn != null)
      {
         _arrayToReturnToPool = null;
         ArrayPool.Shared.Return(toReturn);
      }
   }
}

The second example is RegexWriter, which wraps two ValueListBuilder reference structures that need to be cleared explicitly (since they also manage arrays from the array pool):


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

Conclusion


Removable referenced structures can be thought of as low-space types that have a REAL destructor, as in C ++. It will be invoked as soon as the corresponding instance goes beyond the scope of the using statement (or scope in the case of a using declaration).


Of course, they will not suddenly become popular when writing ordinary, commercial-oriented programs, but if you are creating high-performance, low-level code, you should know about them.


And we also have an article about our conference:



Also popular now: