Давайте посмотрим, что об этом сказано в блоге о предстоящих изменениях в С# 8.0 (версия Visual Studio 2019 Preview 2):


«stack-only структуры появились в С# 7.2. Они чрезвычайно полезны, но при этом их использование тесно связано с ограничениями, например невозможностью реализовывать интерфейсы. Теперь ссылочные структуры можно очищать с помощью метода Dispose внутри них без использования интерфейса IDisposable».


Так и есть: stack-only ref структуры не реализуют интерфейсы, иначе возникала бы вероятность их упаковки. Следовательно, они не могут реализовывать IDisposable, и мы не можем использовать эти структуры в операторе using:


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

ref struct Book : IDisposable
{
   public void Dispose()
   {
   }
}

Попытка запустить этот код приведёт к ошибке компиляции:


Error CS8343 'Book': ref structs cannot implement interfaces

Однако теперь, если мы добавим публичный метод Dispose к ссылочной структуре, оператор using магическим образом примет её, и всё скомпилируется:


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

ref struct Book
{
   public void Dispose()
   {
   }
}

Более того, благодаря изменениям в самом операторе теперь можно использовать using в более краткой форме (так называемые объявления using):


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

Но… зачем?


Это — длинная история, но в целом явная очистка (детерминированная финализация) предпочтительнее, чем неявная (недетерминированная финализация). Это понятно на интуитивном уровне. Лучше явно очистить ресурсы как можно скорее (вызвав Close, Dispose или оператор using), вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь» (когда сама среда запустит финализаторы).


Поэтому, при создании типа, владеющего неким ресурсом, лучше предусмотреть возможность очистки явным образом. В С# это очевидно интерфейс IDisposable и его метод Dispose.


Примечание. Не забывайте, что в случае ссылочных структур используется только явная очистка, поскольку определение финализаторов для них невозможно.


Давайте рассмотрим иллюстративный пример обычной «обёртки для пула неуправляемой памяти». Она занимает минимально возможное место (куча не используется совсем) именно благодаря ссылочной структуре, предназначенной для людей, помешанных на производительности:


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

Поскольку в обёртку заключён неуправляемый ресурс, для очистки после использования мы применяем метод Dispose. Таким образом, пример выглядит как-то так:


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

Это неудобно, поскольку нужно помнить о вызове Dispose. Кроме того, это болезненное решение, поскольку обработка исключений должным образом здесь неприменима. Поэтому, для того чтобы Dispose мог быть вызван изнутри, ввели оператор using. Однако ранее, как уже говорилось, применять его в этой ситуации было нельзя.


Но в С# 8.0 можно использовать преимущества оператора using по полной:


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

При этом код стал лаконичнее благодаря объявлениям:


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

Два других примера внизу (значительная часть кода опущена для краткости) взяты из репозитория CoreFX.


Первый пример – ссылочная структура ValueUtf8Converter, которая оборачивает массив byte[] из пула массивов:


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

Второй пример – RegexWriter, оборачивающий две ссылочные структуры ValueListBuilder, которые необходимо очистить явным образом (поскольку они тоже управляют массивами из пула массивов):


internal ref struct RegexWriter
{
   ...
   private ValueListBuilder<int> _emitted;
   private ValueListBuilder<int> _intStack;
   ...

   public void Dispose()
   {
      _emitted.Dispose();
      _intStack.Dispose();
   }
}

Заключение


Удаляемые ссылочные структуры можно рассматривать как занимающие мало место типы, у которых есть РЕАЛЬНЫЙ деструктор, как в C++. Он будет задействован, как только соответствующий экземпляр выйдет за пределы области оператора using (или области видимости в случае объявления using).


Конечно, они не станут внезапно популярными при написании обычных, ориентированных на коммерческие цели программ, но, если вы создаёте высокопроизводительный, низкоуровневый код, о них стоит знать.


А ещё у нас есть статья про нашу конференцию:

Комментарии (42)


  1. MooNDeaR
    08.04.2019 11:33

    Скоро С++ и С# встретятся и влюблятся :) Очень жду дитя, которое они породят :)


    1. sidristij Автор
      08.04.2019 11:38

      C++/CLI? Оно настолько одиноко, что никому не нужно за очень редким исключением :)


    1. unnutz
      08.04.2019 11:51

      image


    1. snuk182
      08.04.2019 17:46
      +1

      Они близкие родственники. Повод задуматься.


  1. GrimMaple
    08.04.2019 13:34

    Лучше явно очистить ресурсы как можно скорее (вызвав Close, Dispose или оператор using), вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь» (когда сама среда запустит финализаторы).

    А точнее, которая может произойти никогда. MSDN намекает на то, что Dispose нужен мягко говоря для другого — не просто что-то закрыть, а освободить неуправляемые ресурсы, которые иначе освобождены никогда не будут.


    1. RyDmi
      09.04.2019 02:44

      У автора правильно написано. Освобождение ресурса может быть написано в финализаторе без всякого Dispose. Паттерн нужен в первую очередь для детерминированной очистки ресурсов.


      1. RyDmi
        09.04.2019 02:45

        Пардон, "детерминированного освобождения ресурсов"


      1. GrimMaple
        09.04.2019 22:48

        А если

        public struct something : IDisposable

        то в каком именно финализаторе он освободится, в несуществующем?


        1. RyDmi
          09.04.2019 23:51

          Ещё раз про статью в msdn: там описано исходное назначение паттерна — детерминированное освобождение ресурса. О том что stack-only struct поддерживает только явную очистку захваченных ресурсов у автора также указано в статье, но в msdn не об этом. А для приведённого вами примера характерно использование Dispose для работы с управляемыми ресурсами через Dispose (например, локальный захват локера на запись и его высвобождение в Dispose). В том же случае, если вы ссылаетесь на неуправляемый ресурс в структуре (обычной как в примере, не ссылочной), то это как минимум может привести к нарушению семантики владения ресурсом при копировании структуры (неявном опять таки). Да, согласен, в данном случае финализатора нет и есть хорошая возможность выстрелить в ногу из-за отсутствия финализатора и Dispose тут как бы решает проблему, но, тем ни менее, это не отменяет исходное назначение паттерна.


          1. GrimMaple
            10.04.2019 02:17

            Еще раз о том, что написано у автора:

            вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь»

            И о моей поправке:
            А точнее, которая может произойти никогда

            Если не вызывать Dispose явно (или через using), то очистка может не произойти никогда — если это struct (кстати, любая struct). И вы получите утечку памяти/хэндла/еще каких гадостей. Мне кажется, вы совершенно проигнорировали мою исходную позицию, и вместо этого теперь пытаетесь сказать мне то же самое.
            И дело тут, кмк, не в
            Лучше явно очистить ресурсы как можно скорее

            А в том, что если Dispose не вызвать — вы потенциально простреливаете себе ногу.


            1. RyDmi
              10.04.2019 03:35

              Еще раз о том, что написано у автора

              Тут согласен — формулировка неверная, но лишь отчасти (если ресурс используемый в этой ref структуре не обернут в IDisposable объект хотя и абзац был про вообще, а не структуры в частности). Т.е. если там FileStream, то верно, а если нативный буфер, то нет.
              Мне кажется, вы совершенно проигнорировали мою исходную позицию, и вместо этого теперь пытаетесь сказать мне то же самое

              Я отвечал именно на эту вот фразу:
              MSDN намекает на то, что Dispose нужен мягко говоря для другого — не просто что-то закрыть, а освободить неуправляемые ресурсы, которые иначе освобождены никогда не будут

              C этим тезисом я не совсем согласен — в msdn о другом, т.к. если посмотреть на пример в статье, то по нему видно, что даже не вызвав Dispose мы всё равно с течением времени освободим ресурс, но недетерминировано (разумеется, при корректной реализации паттерна, приведенной в качестве примера). В msdn-статье нет ни слова о структурах, т.к. для обычных паттерн юзается не по назначению (и, вообще, зачем нужны явные umanaged в структуре(?), явный антипаттерн и источник проблем за редкими исключениями), а ref появились уже позже и тут вызов Dispose обязателен.
              Вобщем, я думаю, вопрос исчерпан.

              почему 'не совсем согласен'
              Немного поясню почему «не совсем согласен»: всё-таки могут быть edge-кейсы, когда ресурс действительно может стать zombie. Если он уплыл в GC/2 и долго там не собирался мусор, а потом приложение завершается, но финализаторы не отрабатывают в установленный таймаут, а ресурсом был ком-объект через DCOM (вне процесса). Или сайт в пуле IIS хостится, а umnaged-ресурс не CFO. Но это скорее нештатные кейсы.


              1. GrimMaple
                10.04.2019 15:07
                -1

                формулировка неверная, но лишь отчасти

                С каких пор у boolean появилось, помимо истины/лжи, третье значение «может быть»?
                в msdn о другом

                Читаем по ссылке:
                Provides a mechanism for releasing unmanaged resources.
                Перевод: Предоставляет механизм для отчистки неуправляемых ресурсов. Нету ни слова о детерменированности, вы себе ее тут придумываете.
                что даже не вызвав Dispose мы всё равно с течением времени освободим ресурс, но недетерминировано

                Не освободим, если это struct. Я это уже говорил, но вы решили об этом забыть.
                и, вообще, зачем нужны явные umanaged в структуре(?), явный антипаттерн и источник проблем за редкими исключениями

                Антипаттерн — это использовать публичный протокол, который требует явной очистки ресурсов, и потом эти ресурсы не очищать. Также антипаттерн — это надеяться на детали реализации, что кто-то за вами там что-то почистит когда-нибудь. А еще антипаттерн — это использовать классы и гонять по ссылке то, что может спокойно себе полежать на стэке:
                public struct SomeCThing : IDisposable
                {
                    [DllImport("c.dll")]
                    private static extern IntPtr create_c_thing(...);
                
                    private IntPtr handle;
                    // Дальше идут обвязочные методы
                }


                1. RyDmi
                  10.04.2019 20:08

                  Читаем по ссылке:

                  Угу, читаем прям вот следующий абзац
                  Use the Dispose method of this interface to explicitly release unmanaged resources in conjunction with the garbage collector

                  Не освободим, если это struct. Я это уже говорил, но вы решили об этом забыть.

                  Попробуйте прочитать весь абзац из которого вырвана эта фраза и понять к чему она относится. Не вижу смысла в очередной раз повторяться.
                  Антипаттерн — это использовать публичный протокол, который требует явной очистки ресурсов, и потом эти ресурсы не очищать. Также антипаттерн — это надеяться на детали реализации, что кто-то за вами там что-то почистит когда-нибудь.

                  А я разве где-то писал о том, что явно диспозить объект не нужно? Я говорил об исходном назначении паттерна, описанного в статье msdn.
                  Касательно структур с unmanaged: да, иногда очень надо для совсем критичных мест и писаться этот участок должен очень аккуратно, т.к. поломать стейт unmanaged ресурса очень легко, если он не весь идемпотентный и требуется хранить даже минимально мутабельный стейт внутри объекта, владеющего unamanaged ресурсом. Для общего случая работы с Unmanaged это скорее антипаттерн.

                  Пример
                  Before — был какой-то метод с кучей вызовов в Unmanaged
                  After — кто-то его порефакторил. Визуально — вполне легальный рефакторинг без претензий компилятора.
                  Т.е. использование unmanaged в структуре накладывает еще и жесткие ограничения на работу с ней через ref (о которых компилятор не парится), а не только на необходимость вызвать Dispose.
                  ref struct эту проблему тоже не решает, но хотя бы не дает навесить интерфейс и вызывать методы через боксинг (что тоже кораптит стейт), что уже лучше.

                      public unsafe struct CustomStream : IDisposable
                      {
                          private int _size;
                          private int _currentPos;
                          private IntPtr _ptr;
                          private bool _isDisposed;
                  
                          public CustomStream(int initialSize)
                          {
                              _currentPos = 0;
                              _size = initialSize;
                              _ptr = Marshal.AllocHGlobal(initialSize);
                              _isDisposed = false;
                          }
                          
                          public void Append(byte data)
                          {
                              if (_currentPos < _size)
                              {
                                  Marshal.WriteByte(_ptr, _currentPos, data);
                                  // тут вот мы изменяем наш стейт
                                  _currentPos++;
                              }
                          }
                  
                          public void Dispose()
                          {
                              if (!_isDisposed)
                              {
                                  Marshal.FreeHGlobal(_ptr);
                                  _isDisposed = true;
                              }
                          }
                      }
                  
                      public class Example
                      {
                          public void Before()
                          {
                              using (CustomStream stream = new CustomStream(1024))
                              {
                                  stream.Append(0);
                                  stream.Append(1);
                                  // всё ОК
                              }
                          }
                  
                          public void After()
                          {
                              using (CustomStream stream = new CustomStream(1024))
                              {
                                  WriteHeaderBlock(stream);
                                  WriteClosingBlock(stream); // вот тут уже кораптим стейт
                              }
                          }
                  
                          public void WriteHeaderBlock(CustomStream stream)
                          {
                              stream.Append(0);
                          }
                  
                          public void WriteClosingBlock(CustomStream stream)
                          {
                              stream.Append(1);
                          }
                      }
                  


    1. mayorovp
      09.04.2019 10:18

      Ну, по части освобождения ресурсов и всего что с ним связано в MSDN написано нечто очень странное, а местами — устаревшее.

      В реальности метод Dispose может использоваться для чего угодно. При желании, им можно полностью заменить любые повторяющиеся блоки finally, что стало ещё актуальнее с появлением краткой формы оператора using.



  1. Ascar
    08.04.2019 15:29

    Походу надежнее написать try...finally, чтобы избежать возможных сюрпризов от компилятора наподобие (IDisposable)book).Dispose();


    1. oblomov86
      09.04.2019 12:47

      ref-структура не может быть упакована, а при таком касте произойдет упаковка. Именно поэтому ref-структуры не поддерживают интерфейсы. В статье об этом написано.


      1. Ascar
        09.04.2019 23:01
        -1

        Я привел пример работы using со структурой, во что компилятор решает развернуть код в блоке finally:

         book book = default(book);
                try
                {
                }
                finally
                {
                    ((IDisposable)book).Dispose(); //тут сюрприз, если book обычная IDisposable структура
                }


        Я же не говорю что он тоже самое делает с ref. Если компилятор работает лучше с ref структурой, то это только хорошо.


        1. oblomov86
          10.04.2019 09:11

          Лучше посмотрите IL-код, сгенереный для исходного варианта с using, там нет боксинга и для обычных структур.


          1. Ascar
            10.04.2019 17:25

            Я ниже уже писал, что приведение сгенерится, а упаковки невидно. Причем если будет хоть какая логика, наподобие инициализации поля, то компилятор тупо скопирует значение структуры в новую переменную. И чем вам не понравился после этого вариант с ручным try...finally?


            1. oblomov86
              10.04.2019 17:48

              IL-код из блока finally для обоих случаев.
              Вариант с юзингом:
              IL_000d: ldloca.s b
              IL_000f: constrained. C/book
              IL_0015: callvirt instance void [System.Runtime]System.IDisposable::Dispose()

              Вариант с ручным try-finally:
              IL_000e: ldloc.0
              IL_000f: box C/book
              IL_0014: callvirt instance void [System.Runtime]System.IDisposable::Dispose()


              1. Ascar
                10.04.2019 18:09

                Под ручным то не надо приводить, просто вызвать Dispose. К сожалению даже так используется еще одна переменная структура… Вывод: нельзя использовать этот механизм со структурами.


      1. NoofSaeidh
        10.04.2019 12:03

        ref struct не может быть упакована только с точки зрения языка. MSIL ничего о ref struct не знает. Кстати упаковать ref struct не так уж сложно с помощью активатора, если очень хочется.


    1. yarosroman
      09.04.2019 13:00

      Я вас огорчу, тут упаковки не будет. Тут будет callvirt предваренный constrained опкодом. И далее зависит от JITа, если если метод определен в структуре, то упаковки не будет, будет прямой вызов метода, а если нет (для структуры это вызов методов ValueType не переопределенных в структуре) то упаковка и вызов виртуального метода.


      1. qutrix
        09.04.2019 13:58

        все как Вы говорите, только вот выглядит так, будто JIT боксирует, судя по результатам
        запускал и получал те же результаты на netcore2.2, netcore3.0


        1. mayorovp
          09.04.2019 17:50

          Не «боксирует», а копирует. И не JIT, а компилятор.


          1. qutrix
            09.04.2019 21:39

            ох, да, вижу
            тогда у меня следующие вопросы:
            1) зачем компилятор это делает (копирует переменную)? Это не то, что я ожидаю
            2) почему в этом случае Dispose(), который в finally, вызывается через constrained? Ведь просто вызов (не override) метода у структуры — это просто call. Если структура приводится к интерфейсу — тогда будет box, которого я не вижу в finally

            В общем: какую магию применяет компилятор по отношению к структурам в using?


            1. mayorovp
              09.04.2019 22:51

              Смысл constrained call — в том, чтобы вызвать не просто Dispose, а IDisposable::Dispose, но без приведения типа.

              Это важно при явной реализации метода, и не играет никакой роли при публичной реализации.


              1. qutrix
                10.04.2019 12:09

                Это важно при явной реализации метода, и не играет никакой роли при публичной реализации.

                вот этого не понял… Что за публичная реализация? Можно пример, в котором видна разница Dispose и IDisposable::Dispose вызовов?


                1. mayorovp
                  10.04.2019 13:11

                  Если метод реализован явно (explicit), т.е. не как public void Dispose(), а как void IDisposable.Dispose() — то вызвать его можно либо через каст к интерфейсу (что для структуры будет упаковкой), либо через constrained call.


                  А копия делается для того, чтобы не изменилось наблюдаемое поведение.


              1. qutrix
                10.04.2019 12:10

                и все же: зачем компилятор делает копирование?


            1. yarosroman
              09.04.2019 23:52

              Компилятор делает защитную копию, потом вызывается callvirt предваренный constraited. Из-за защитной копии, все уверены, что будет упаковка, выберите на шарплабе компиляцию в C# и увидите. однако, у вас в примере в типе только булево поле оно копируется в защитную копию, однако если у вас там ссылка на неуправляемый ресурс, применение после using черевато.


            1. yarosroman
              09.04.2019 23:56

              кстати недавно было обсуждение этого вопроса, вот советую почитать sergeyteplyakov.blogspot.com/2019/02/c.html?m=1


          1. qutrix
            09.04.2019 22:15

            del


      1. Ascar
        09.04.2019 23:03

        А я и не говорю что в ref будет упаковка.

        upd: вы кстати пишите что если структура(не ref), то упаковки не будет. Хочу уточнить, если я обычную структуру (:IDisposable) использую вместе с using, то упаковки не будет?


        1. RyDmi
          10.04.2019 00:55

          Не будет. Сделали специальную оптимизацию в обход спецификации: https://stackoverflow.com/questions/2412981/if-my-struct-implements-idisposable-will-it-be-boxed-when-used-in-a-using-statem/2413844#2413844


          1. yarosroman
            10.04.2019 02:43

            Никакого обхода нет. Фактически оптимизация заключается в удалении преобразования (о чем написал Эрик в блоге, ссылка есть в его ответе), ну и constrained вызовы специально сделали в cil, как раз для подобного. Естественно, когда вы структуру присваиваете переменной интересного типа и потом вызываете метод, то тут и будут все прелести боксинга.


          1. Ascar
            10.04.2019 15:33

            Компилятор генерит привидение к Idisposable, но вот при запуске похоже что упаковки нет


  1. qutrix
    08.04.2019 18:42
    +2

    Примечание. Не забывайте, что в случае ссылочных структур используется только явная очистка, поскольку определение финализаторов для них невозможно.

    Сказано так, будто у обычных структур есть финализатор


  1. KaiOvas
    09.04.2019 13:06

    Довольно не однозначное впечатление от этой фичи языка.
    С одной стороны — это (ref структуры) требуется в том случае когда нужна небольшая обертка вокруг неуправляемых ресурсов и эта тонкая обертка позволяет детерминированно освобождать их НО с другой стороны очень нужно что бы компилятор как минимум предупреждал что для такой структуры не был вызван метод Dispose и/или эта структура требует использования using оператора или же студия каким либо иным способом (какой — либо анализатор кода ) давала возможность разрабочтику понять что у него потенциальная утечка ресурсов.


    1. sidristij Автор
      09.04.2019 17:14

      Я бы даже сказал, что у них возможен финализатор. Только вызываться он должен по очистке занятой зоны стека.


  1. Ascar
    10.04.2019 15:32

    del