Правильное использование интерфейса IDisposable


1659

Из документации Microsoft я знаю, что «основное» использование IDisposableинтерфейса - очистка неуправляемых ресурсов.

Для меня «неуправляемый» означает такие вещи, как соединения с базой данных, сокеты, дескрипторы окон и т. Д. Но я видел код, в котором Dispose()метод реализован для освобождения управляемых ресурсов, что мне кажется избыточным, поскольку сборщик мусора должен позаботиться о это для тебя.

Например:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }

Мой вопрос заключается в том, делает ли сборщик мусора свободную память используемой MyCollectionбыстрее, чем обычно?

редактирование : до сих пор люди опубликовали несколько хороших примеров использования IDisposable для очистки неуправляемых ресурсов, таких как соединения с базой данных и растровые изображения. Но предположим, что _theListв приведенном выше коде содержится миллион строк, и вы хотели освободить эту память сейчас , а не ждать сборщика мусора. Будет ли приведенный выше код выполнить это?


34
Мне нравится принятый ответ, потому что он говорит вам правильный «шаблон» использования IDisposable, но, как сказал ОП в своем редактировании, он не отвечает на его предполагаемый вопрос. IDisposable не «вызывает» GC, он просто «помечает» объект как разрушаемый. Но каков реальный способ освободить память «прямо сейчас» вместо того, чтобы ждать, пока GC начнет действовать? Я думаю, что этот вопрос заслуживает большего обсуждения.
Punit Vora

40
IDisposableничего не помечает. DisposeМетод делает то , что он должен сделать , чтобы очистить ресурсы , используемые экземпляром. Это не имеет ничего общего с GC.
Джон Сондерс

4
@John. Я понимаю IDisposable. И именно поэтому я сказал, что принятый ответ не отвечает на заданный OP вопрос (и последующее редактирование) о том, поможет ли IDisposable в <i> освобождении памяти </ i>. Так IDisposableкак не имеет никакого отношения к освобождению памяти, а только к ресурсам, то, как вы сказали, нет нужды вообще устанавливать для управляемых ссылок значение null, как это делал OP в своем примере. Таким образом, правильный ответ на его вопрос: «Нет, это не помогает освободить память быстрее. Фактически, это не помогает освободить память вообще, только ресурсы». Но в любом случае, спасибо за ваш вклад.
Punit Vora

9
@desigeek: если это так, то вам не следовало говорить «IDisposable не вызывает» GC, он просто «помечает» объект как разрушаемый »
Джон Сондерс,

5
@desigeek: не существует гарантированного способа освобождения памяти детерминистически. Вы можете вызвать GC.Collect (), но это вежливый запрос, а не требование. Все выполняющиеся потоки должны быть приостановлены для продолжения сборки мусора - прочитайте концепцию безопасных точек .NET, если хотите узнать больше, например, msdn.microsoft.com/en-us/library/678ysw69(v=vs.110). aspx . Если поток не может быть приостановлен, например, из-за вызова неуправляемого кода, GC.Collect () может вообще ничего не делать.
Бетонная олуша

Ответы:


2609

Задача Dispose - освободить неуправляемые ресурсы. Это нужно сделать в какой-то момент, иначе они никогда не будут очищены. Сборщик мусора не знает, как вызвать DeleteHandle()переменную типа IntPtr, он не знает, нужно ли ему вызывать DeleteHandle().

Примечание . Что такое неуправляемый ресурс ? Если вы нашли его в Microsoft .NET Framework: он управляется. Если вы сами ковырялись в MSDN, это неуправляемо. Все, что вы использовали с помощью вызовов P / Invoke, чтобы выйти из приятного удобного мира всего, что доступно вам в .NET Framework, неуправляемо - и теперь вы несете ответственность за его очистку.

Созданный вами объект должен предоставить некоторый метод, который может вызывать внешний мир, для очистки неуправляемых ресурсов. Метод можно назвать как угодно:

public void Cleanup()

или

public void Shutdown()

Но вместо этого есть стандартизированное имя для этого метода:

public void Dispose()

Был даже создан интерфейс IDisposable, который имеет только один метод:

public interface IDisposable
{
   void Dispose()
}

Таким образом, вы заставляете ваш объект предоставлять IDisposableинтерфейс, и таким образом вы обещаете, что написали этот единственный метод для очистки ваших неуправляемых ресурсов:

public void Dispose()
{
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}

И вы сделали. За исключением того, что вы можете сделать лучше.


Что если ваш объект выделил 250MB System.Drawing.Bitmap (то есть, управляемый .NET класс Bitmap) в качестве буфера кадра? Конечно, это управляемый объект .NET, и сборщик мусора освободит его. Но вы действительно хотите оставить 250 МБ памяти, просто сидя там - ожидая, когда сборщик мусора в конце концов придет и освободит его? Что если есть открытое соединение с базой данных ? Конечно, мы не хотим, чтобы это соединение оставалось открытым, ожидая, пока GC завершит объект.

Если пользователь вызвал Dispose()(то есть он больше не планирует использовать объект), почему бы не избавиться от этих расточительных растровых изображений и соединений с базой данных?

Итак, теперь мы будем:

  • избавиться от неуправляемых ресурсов (потому что мы должны), и
  • избавиться от управляемых ресурсов (потому что мы хотим быть полезными)

Итак, давайте обновим наш Dispose()метод, чтобы избавиться от этих управляемых объектов:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

И все хорошо, кроме того, что вы можете сделать лучше !


Что если человек забыл позвонить Dispose()на ваш объект? Тогда они будут пропускать некоторые неуправляемые ресурсы!

Примечание. Они не будут пропускать управляемые ресурсы, поскольку в конечном итоге сборщик мусора будет работать в фоновом потоке и освобождать память, связанную с любыми неиспользуемыми объектами. Это будет включать ваш объект и любые управляемые объекты, которые вы используете (например, Bitmapи DbConnection).

Если человек забыл позвонить Dispose(), мы все равно можем сохранить его сало! У нас все еще есть способ вызвать это для них: когда сборщик мусора, наконец, приступает к освобождению (т.е. завершению) нашего объекта.

Примечание. Сборщик мусора в конечном итоге освободит все управляемые объекты. Когда это происходит, он вызывает Finalize метод объекта. GC не знает или не заботится о вашем методе утилизации . Это было просто имя, которое мы выбрали для метода, который мы вызываем, когда хотим избавиться от неуправляемых вещей.

Уничтожение нашего объекта сборщиком мусора - идеальное время, чтобы освободить эти надоедливые неуправляемые ресурсы. Мы делаем это путем переопределения Finalize()метода.

Примечание. В C # вы явно не переопределяете Finalize()метод. Вы пишете метод , который выглядит как C ++ деструктора , и компилятор принимает , что быть вашей реализацией Finalize()методы:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

Но в этом коде есть ошибка. Видите ли, сборщик мусора работает в фоновом потоке ; Вы не знаете порядок, в котором уничтожены два объекта. Вполне возможно, что в вашем Dispose()коде управляемого объекта, от которого вы пытаетесь избавиться (потому что вы хотели быть полезным), больше нет:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

Так что вам нужен способ Finalize()сказать, Dispose()что он не должен касаться каких-либо управляемых ресурсов (потому что их там больше не может быть ), при этом освобождая неуправляемые ресурсы.

Стандартный шаблон для этого - иметь Finalize()и Dispose()оба вызывать третий (!) Метод; где вы передаете логическое выражение, если вы вызываете его Dispose()(в отличие от Finalize()), то есть безопасно освобождать управляемые ресурсы.

Этот внутренний метод может иметь произвольное имя, например «CoreDispose» или «MyInternalDispose», но традиционно его называют Dispose(Boolean):

protected void Dispose(Boolean disposing)

Но более полезное имя параметра может быть:

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

И вы измените свою реализацию IDisposable.Dispose()метода на:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

и ваш финализатор для:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

Примечание : если ваш объект происходит от объекта, который реализует Dispose, то не забывайте вызывать их базовый метод Dispose при переопределении Dispose:

public override void Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

И все хорошо, кроме того, что вы можете сделать лучше !


Если пользователь вызывает Dispose()ваш объект, то все было убрано. Позже, когда придет сборщик мусора и вызовет Finalize, он снова вызовет Dispose.

Это не только расточительно, но если у вашего объекта есть ненужные ссылки на объекты, которые вы уже удалили из последнего вызова Dispose(), вы попытаетесь утилизировать их снова!

Вы заметите, что в моем коде я осторожно удалял ссылки на объекты, которые я выбрал, поэтому я не пытаюсь вызвать Disposeссылку на ненужный объект. Но это не остановило незаметную ошибку.

Когда пользователь вызывает Dispose(): дескриптор CursorFileBitmapIconServiceHandle уничтожается. Позже, когда запускается сборщик мусора, он снова попытается уничтожить ту же ручку.

protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy 
   ...
}

Способ исправить это - сказать сборщику мусора, что ему не нужно беспокоиться о завершении объекта - его ресурсы уже очищены, и больше не требуется никакой работы. Вы можете сделать это по телефону GC.SuppressFinalize()в Dispose()методе:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

Теперь, когда пользователь позвонил Dispose(), мы имеем:

  • освобожденные неуправляемые ресурсы
  • освобожденные управляемые ресурсы

В GC нет смысла запускать финализатор - обо всем позаботились.

Не могу ли я использовать Finalize для очистки неуправляемых ресурсов?

Документация для Object.Finalizeговорит:

Метод Finalize используется для выполнения операций по очистке неуправляемых ресурсов, удерживаемых текущим объектом, до его уничтожения.

Но в документации MSDN также сказано IDisposable.Dispose:

Выполняет определенные приложением задачи, связанные с освобождением, освобождением или сбросом неуправляемых ресурсов.

Так что это? Какое место для меня, чтобы очистить неуправляемые ресурсы? Ответ:

Это твой выбор! Но выбирай Dispose.

Вы, конечно, можете поместить свою неуправляемую очистку в финализатор:

~MyObject()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //A C# destructor automatically calls the destructor of its base class.
}

Проблема в том, что вы понятия не имеете, когда сборщик мусора дойдет до завершения вашего объекта. Ваш непредоставленные удалось, не-необходимости, Неиспользуемая родные ресурсы будут придерживаться вокруг , пока сборщик мусора в конечном счете не работает. Затем он вызовет ваш метод финализатора; очистка неуправляемых ресурсов. Документация Object.Finalize указывает на это:

Точное время выполнения финализатора не определено. Чтобы обеспечить детерминированное освобождение ресурсов для экземпляров вашего класса, реализуйте метод Close или предоставьте IDisposable.Disposeреализацию.

Это преимущество использования Disposeдля очистки неуправляемых ресурсов; Вы узнаете и контролируете, когда неуправляемые ресурсы очищаются. Их уничтожение является «детерминированным» .


Чтобы ответить на ваш первоначальный вопрос: почему бы не освободить память сейчас, а не тогда, когда GC решит это сделать? У меня есть программное обеспечение для распознавания лиц, которое теперь должно избавиться от 530 МБ внутренних изображений , поскольку они больше не нужны. Когда мы этого не сделаем: машина останавливается.

Бонус Чтение

Для тех, кто любит стиль этого ответа (объясняя почему , и как это становится очевидным), я предлагаю вам прочитать Главу 1 «Основного COM» Дона Бокса:

На 35 страницах он объясняет проблемы использования бинарных объектов и изобретает COM на ваших глазах. Как только вы поймете причину COM, оставшиеся 300 страниц станут очевидными и просто детализируют реализацию Microsoft.

Я думаю, что каждый программист, который когда-либо имел дело с объектами или COM, должен, по крайней мере, прочитать первую главу. Это лучшее объяснение чего-либо.

Дополнительное чтение бонусов

Когда все, что вы знаете, неправильно , Эрик Липперт

Поэтому действительно очень сложно написать правильный финализатор, и лучший совет, который я могу вам дать, - не пытаться .


12
Вы можете сделать лучше - вам нужно добавить вызов GC.SuppressFinalize () в Dispose.
плинтус

55
@Daniel Earwicker: это правда. Microsoft хотела бы, чтобы вы полностью прекратили использовать Win32 и придерживались красиво абстрагируемых, портативных, независимых от устройств вызовов .NET Framework. Если вы хотите покопаться в операционной системе под ней; потому что вы думаете, что знаете, какая ОС работает: вы берете свою жизнь в свои руки. Не каждое приложение .NET работает в Windows или на рабочем столе.
Ян Бойд

34
Это отличный ответ, но я думаю, что он выиграл бы от окончательного перечисления кода для стандартного случая и для случая, когда класс является производным от базового класса, который уже реализует Dispose. например, прочитав здесь ( msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx ), я также запутался в том, что я должен делать при выводе из класса, который уже реализует Dispose ( эй, я новичок в этом).
integra753

5
@GregS и другие: как правило, я бы не стал задавать ссылки на null. Во-первых, это означает, что вы не можете их сделать readonly, а во-вторых, вы должны делать очень уродливые !=nullпроверки (как в примере кода). У вас мог быть флаг disposed, но легче об этом не беспокоиться. .NET GC достаточно агрессивен, так что ссылка на поле xбольше не будет считаться «использованной» к тому времени, когда она пройдет x.Dispose()строку.
Porges

7
На второй странице книги Дона Бокса, которую вы упомянули, он использует пример реализации алгоритма поиска O (1), «детали которого оставлены в качестве упражнения для читателя». Я смеялся.
протри

65

IDisposableчасто используется для использования usingоператора и использования простого способа детерминированной очистки управляемых объектов.

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

6
Лично мне это нравится, но это не совсем соответствует рекомендациям по проектированию фреймворка
Mqp

4
Я бы посчитал это правильным дизайном, потому что он позволяет легко детерминированные области видимости и конструкции / очистки областей, особенно когда он сложным образом смешивается с обработкой исключений, блокировкой и использованием блоков неуправляемых ресурсов. Язык предлагает это как первоклассную функцию.
yfeldblum

Он не совсем соответствует правилам, указанным в FDG, но, безусловно, является допустимым использованием шаблона, так как он требуется для использования «оператором использования».
Скотт Дорман

2
Пока Log.Outdent не выбрасывает, в этом нет ничего плохого.
Даниэль Уорвикер

1
Различные ответы на вопрос: является ли неправомерным использование IDisposable и «использование» в качестве средства получения «ограниченного поведения» для обеспечения безопасности исключений? подробнее рассмотрим, почему разные люди любят / не любят эту технику. Это несколько спорный.
Брайан

44

Цель шаблона Dispose - предоставить механизм для очистки как управляемых, так и неуправляемых ресурсов, и когда это произойдет, зависит от того, как вызывается метод Dispose. В вашем примере использование Dispose на самом деле ничего не делает для удаления, поскольку очистка списка не влияет на удаление этой коллекции. Аналогичным образом, вызовы для установки переменных в null также не влияют на сборщик мусора.

Вы можете взглянуть на эту статью для более подробной информации о том, как реализовать шаблон Dispose, но в основном это выглядит так:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Наиболее важным здесь является метод Dispose (bool), который на самом деле работает при двух разных обстоятельствах:

  • утилизация == true: метод был вызван прямо или косвенно кодом пользователя. Управляемые и неуправляемые ресурсы могут быть утилизированы.
  • пинг == ложь: метод был вызван средой выполнения из финализатора, и вы не должны ссылаться на другие объекты. Только неуправляемые ресурсы могут быть утилизированы.

Проблема с тем, чтобы просто позволить GC позаботиться о выполнении очистки, заключается в том, что у вас нет реального контроля над тем, когда GC будет запускать цикл сбора (вы можете вызвать GC.Collect (), но на самом деле не следует), поэтому ресурсы могут остаться вокруг дольше, чем нужно. Помните, что вызов Dispose () на самом деле не вызывает цикл сбора или каким-либо образом заставляет GC собирать / освобождать объект; он просто предоставляет средства для более детерминированной очистки используемых ресурсов и сообщает GC, что эта очистка уже выполнена.

Весь смысл IDisposable и шаблона dispose не заключается в немедленном освобождении памяти. Единственный раз, когда у вызова Dispose даже есть шанс немедленно освободить память, это когда он обрабатывает сценарий == false и манипулирует неуправляемыми ресурсами. Для управляемого кода память фактически не будет возвращена до тех пор, пока GC не выполнит цикл сбора, который вы действительно не можете контролировать (кроме вызова GC.Collect (), о котором я уже упоминал, не очень хорошая идея).

Ваш сценарий на самом деле недопустим, поскольку строки в .NET не используют никаких неуправляемых ресурсов и не реализуют IDisposable, поэтому нет способа заставить их «очиститься».


4
Разве вы не забыли реализовать финализатор?
Будда

@Budda: Нет, он использует SafeHandle. Нет необходимости в деструкторе.
Хенк Холтерман

9
+1 за добавление сети безопасности для нескольких вызовов в Dispose (). В спецификации сказано, что несколько звонков должны быть безопасными. Слишком много классов Microsoft не могут реализовать это, и вы получаете раздражающее исключение ObjectDisposedException.
Джесси Чисхолм

5
Но Dispose (bool dispose) - это ваш собственный метод в классе SimpleCleanup, который никогда не будет вызываться фреймворком. Поскольку вы называете его только как «true» в качестве параметра, «утилизация» никогда не будет ложной. Ваш код очень похож на пример MSDN для IDisposable, но в нем отсутствует финализатор, как указывал @Budda, и именно отсюда поступит вызов с утилизацией = false.
Yoyo

19

После вызова Dispose не должно быть никаких дальнейших вызовов методов объекта (хотя объект должен допускать дальнейшие вызовы Dispose). Поэтому пример в вопросе глупый. Если вызывается Dispose, то сам объект может быть отброшен. Таким образом, пользователь должен просто отбросить все ссылки на весь этот объект (установить для него значение null), и все связанные с ним внутренние объекты будут автоматически очищены.

Что касается общего вопроса об управляемых / неуправляемых и обсуждения в других ответах, я думаю, что любой ответ на этот вопрос должен начинаться с определения неуправляемого ресурса.

Все сводится к тому, что есть функция, которую вы можете вызвать, чтобы привести систему в состояние, и есть другая функция, которую вы можете вызвать, чтобы вывести ее из этого состояния. Теперь, в типичном примере, первая может быть функцией, которая возвращает дескриптор файла, а вторая может быть вызовом CloseHandle.

Но - и это ключ - они могут быть любой подходящей парой функций. Один создает государство, другой разрушает его. Если состояние было построено, но еще не снесено, то существует экземпляр ресурса. Вы должны организовать, чтобы демонтаж происходил в нужное время - ресурс не управляется CLR. Единственный автоматически управляемый тип ресурса - это память. Есть два вида: GC и стек. Типы значений управляются стеком (или путём перехода внутри ссылочных типов), а ссылочные типы управляются GC.

Эти функции могут вызывать изменения состояния, которые могут свободно чередоваться, или могут нуждаться в идеальном вложении. Изменения состояния могут быть поточно-безопасными или нет.

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

Можно собрать мусора, чтобы очистить ваши неуправляемые ресурсы. Но только если функции изменения состояния являются поточно-ориентированными и два состояния могут иметь время жизни, которое перекрывается любым образом. Таким образом, пример ресурса правосудия НЕ должен иметь финализатор! Это просто никому не поможет.

Для таких видов ресурсов вы можете просто реализовать IDisposableбез финализатора. Финализатор абсолютно необязателен - так и должно быть. Это скрыто или даже не упоминается во многих книгах.

Затем вы должны использовать это usingутверждение, чтобы иметь возможность убедиться, что оно Disposeвызывается. По сути, это похоже на соединение со стеком (так как финализатор для GC, usingэто для стека).

Недостающая часть заключается в том, что вы должны вручную написать Dispose и вызвать его для ваших полей и вашего базового класса. Программисты C ++ / CLI не должны этого делать. Компилятор пишет это для них в большинстве случаев.

Существует альтернатива, которую я предпочитаю для состояний, которые прекрасно встраиваются и не являются поточно-ориентированными (кроме всего прочего, избегание IDisposable избавляет вас от проблемы с кем-то, кто не может устоять перед добавлением финализатора в каждый класс, реализующий IDisposable). ,

Вместо того, чтобы писать класс, вы пишете функцию. Функция принимает делегата для обратного вызова:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

И тогда простой пример будет:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

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

Этот метод менее полезен, если ресурс того типа, который может иметь перекрывающиеся времена жизни, потому что тогда вы хотите иметь возможность построить ресурс A, затем ресурс B, затем убить ресурс A, а затем убить ресурс B. Вы не можете сделать это если вы заставили пользователя идеально вложить это. Но тогда вам нужно использовать IDisposable(но все же без финализатора, если вы не реализовали безопасность потоков, что не является бесплатным).


re: «После вызова метода Dispose не должно быть никаких дальнейших вызовов методов объекта». «Должен» быть оперативным словом. Если у вас ожидают асинхронные действия, они могут появиться после удаления вашего объекта. Вызывает ObjectDisposedException.
Джесси Чисхолм

Ваш, похоже, единственный ответ, кроме моего, который касается идеи о том, что неуправляемые ресурсы инкапсулируют состояние, которое GC не понимает. Однако ключевым аспектом неуправляемого ресурса является то, что один или несколько объектов, состояние которых может нуждаться в очистке своего состояния, могут продолжать существовать, даже если объект, который «владеет» ресурсом, не существует. Как тебе мое определение? Довольно похоже, но я думаю, что это делает «ресурс» немного более существительным (это «согласие» внешнего объекта на изменение своего поведения в обмен на уведомление о том, когда его услуги больше не нужны)
суперкат

@supercat - если вам интересно, я написал следующее сообщение через пару дней после того, как написал ответ выше: smellegantcode.wordpress.com/2009/02/13/…
Даниэль Уорвикер,

1
@DanielEarwicker: интересная статья, хотя я могу вспомнить, по крайней мере, один тип неуправляемых ресурсов, которые вы на самом деле не освещаете: подписку на события из долгоживущих объектов. Подписки на события являются взаимозаменяемыми, но даже если бы память была неограниченной, отказ от их утилизации может быть дорогостоящим. Например, перечислителю для коллекции, которая допускает изменение во время перечисления, может потребоваться подписка на обновления уведомлений из коллекции, и коллекция может обновляться много раз за время своего существования. Если
счетчики

1
Пара операций enterи exitявляется основой того, как я думаю о ресурсе. Подписка / отказ от подписки на события должны вписываться в это без труда. С точки зрения ортогональных / взаимозаменяемых характеристик он практически неотличим от утечки памяти. (Это неудивительно, поскольку подписка просто добавляет объекты в список.)
Даниэль Эрвикер,

17

Сценарии, которые я использую IDisposable: очистить неуправляемые ресурсы, отписаться о событиях, закрыть соединения

Идиома, которую я использую для реализации IDisposable ( не потокобезопасен ):

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

Полное объяснение паттерна можно найти по адресу msdn.microsoft.com/en-us/library/b1yfkh5e.aspx
ЛицензияQ

3
Не следует включать финализатор, если у вас нет неуправляемых ресурсов. Даже в этом случае предпочтительной реализацией является перенос неуправляемого ресурса в SafeHandle.
Дейв Блэк

11

Да, этот код является полностью избыточным и ненужным, и он не заставляет сборщик мусора делать то, что он не делал бы иначе (когда экземпляр MyCollection выходит из области видимости, то есть.) Особенно это касается .Clear()вызовов.

Ответ на ваши изменения: вроде. Если я сделаю это:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Это функционально идентично этому для целей управления памятью:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Если вам действительно очень нужно освободить память в этот самый момент, звоните GC.Collect(). Там нет причин делать это здесь, хотя. Память будет освобождена, когда это необходимо.


2
Re: «Память будет освобождена, когда это необходимо.» Скорее сказать, «когда GC решит, что это необходимо». Вы можете увидеть проблемы с производительностью системы, прежде чем GC решит, что память действительно нужна. Освободить его сейчас не обязательно, но может быть полезно.
Джесси Чисхолм

1
В некоторых угловых случаях удаление ссылок в коллекции может ускорить сборку мусора для элементов, на которые они ссылаются. Например, если большой массив создается и заполняется ссылками на новые объекты меньшего размера, но это не требуется очень долго после этого, отказ от массива может привести к тому, что эти элементы будут храниться до следующего уровня 2 GC, в то время как его обнуление первым может сделать предметы подходящими для следующего уровня 0 или уровня 1 GC. Конечно, иметь большие недолговечные объекты в куче больших объектов все равно непросто (мне не нравится дизайн), но ...
суперкат

1
... обнуление таких массивов перед тем, как оставить их, иногда уменьшает влияние GC.
суперкат

11

Если MyCollectionвы все равно будете собирать мусор, вам не нужно его утилизировать. Это просто увеличит производительность ЦП, чем необходимо, и может даже сделать недействительным некоторый предварительно рассчитанный анализ, который сборщик мусора уже выполнил.

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

РЕДАКТИРОВАТЬ В ответ на комментарий Скотта:

Единственный раз, когда на показатели производительности GC влияют, это когда вызывается [sic] GC.Collect ()

Концептуально GC поддерживает представление графа ссылок объекта и всех ссылок на него из стековых фреймов потоков. Эта куча может быть довольно большой и занимать много страниц памяти. В качестве оптимизации GC кеширует свой анализ страниц, которые вряд ли изменятся очень часто, чтобы избежать ненужного повторного сканирования страницы. GC получает уведомление от ядра при изменении данных на странице, поэтому он знает, что страница грязная и требует повторного сканирования. Если коллекция находится в Gen0, то вероятно, что другие вещи на странице тоже меняются, но это менее вероятно в Gen1 и Gen2. Как ни странно, эти перехватчики не были доступны в Mac OS X для команды, которая перенесла GC на Mac, чтобы заставить плагин Silverlight работать на этой платформе.

Еще один момент против ненужной утилизации ресурсов: представьте ситуацию, когда процесс выгружается. Представьте также, что процесс запущен в течение некоторого времени. Скорее всего, многие страницы памяти этого процесса были перенесены на диск. По крайней мере, они больше не находятся в кеше L1 или L2. В такой ситуации приложение, которое выгружается, не имеет смысла выгружать все эти данные и кодовые страницы обратно в память, чтобы «высвободить» ресурсы, которые будут освобождены операционной системой в любом случае, когда процесс завершится. Это относится к управляемым и даже определенным неуправляемым ресурсам. Только ресурсы, которые поддерживают не фоновые потоки, должны быть удалены, иначе процесс останется живым.

Теперь во время обычного выполнения существуют временные ресурсы, которые необходимо правильно очистить (так как @fezmonkey указывает на соединения с базой данных, сокеты, дескрипторы окон ), чтобы избежать неуправляемых утечек памяти. Это те вещи, которые должны быть уничтожены. Если вы создаете какой-то класс, которому принадлежит поток (и я имею в виду, что он создал его и, следовательно, отвечает за его остановку, по крайней мере, благодаря моему стилю кодирования), то этот класс, скорее всего, должен реализовать IDisposableи разрушить поток во время Dispose.

.NET Framework использует IDisposableинтерфейс как сигнал, даже предупреждая разработчиков, что этот класс должен быть удален. Я не могу думать о каких-либо типах в рамках, которые реализуют IDisposable(исключая явные реализации интерфейса), где удаление является необязательным.


Вызов Dispose совершенно действителен, законен и приветствуется. Объекты, которые реализуют IDisposable, обычно делают это по определенной причине. Единственный раз, когда на показатели производительности GC влияют, это когда вызывается GC.Collect ().
Скотт Дорман

Для многих классов .net удаление «несколько» необязательно, это означает, что отказ от экземпляров «обычно» не вызовет никаких проблем, если не сходить с ума, создавая новые экземпляры и отказываясь от них. Например, код, сгенерированный компилятором для элементов управления, кажется, создает шрифты при создании экземпляров элементов управления и оставляет их при удалении форм; если создать и использовать тысячи элементов управления, это может связать тысячи дескрипторов GDI, но в большинстве случаев элементы управления не создаются и не разрушаются так сильно. Тем не менее, все же следует стараться избегать такого отказа.
суперкат

1
Я подозреваю, что в случае со шрифтами проблема заключается в том, что Microsoft так и не определила, какой объект отвечает за удаление объекта «шрифт», назначенного элементу управления; в некоторых случаях элементы управления могут совместно использовать шрифт с более долгоживущим объектом, поэтому использование элемента управления Dispose для шрифта было бы плохо. В других случаях шрифт будет назначен элементу управления, и нигде больше, поэтому, если элемент управления не располагает его, никто не будет. Между прочим, этой проблемы со шрифтами можно было бы избежать, если бы существовал отдельный одноразовый класс FontTemplate, поскольку элементы управления, похоже, не используют дескриптор GDI своего шрифта.
суперкат

По теме необязательных Dispose()вызовов см .: stackoverflow.com/questions/913228/…
RJ Cuthbertson

7

В приведенном вами примере он все еще не «освобождает память сейчас». Вся память является сборщиком мусора, но это может позволить собирать память в более раннем поколении . Вы должны выполнить несколько тестов, чтобы быть уверенным.


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

Однажды я прочитал код, который был простым RollBack () при ошибке с использованием IDisposable. Класс MiniTx, приведенный ниже, будет проверять флаг Dispose () и, если Commitвызов не произойдет, он будет вызывать Rollbackсам себя. Это добавило слой косвенности, что значительно облегчило понимание и поддержку вызывающего кода. Результат выглядел примерно так:

using( MiniTx tx = new MiniTx() )
{
    // code that might not work.

    tx.Commit();
} 

Я также видел, что тайминг / логирование кода делают то же самое. В этом случае метод Dispose () остановил таймер и зарегистрировал, что блок вышел.

using( LogTimer log = new LogTimer("MyCategory", "Some message") )
{
    // code to time...
}

Итак, вот пара конкретных примеров, которые не выполняют никакой неуправляемой очистки ресурсов, но успешно используют IDisposable для создания более чистого кода.


Взгляните на пример @Daniel Earwicker с использованием функций более высокого порядка. Для бенчмаркинга, хронометража, логирования и т. Д. Это кажется гораздо проще.
Алуан Хаддад


6

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

Открытый класс LargeStuff
  Реализует IDisposable
  Private _Large as string ()

  «Какой-то странный код, который означает, что _Large теперь содержит несколько миллионов длинных строк.

  Public Sub Dispose () Реализует IDisposable.Dispose
    _Large = Nothing
  End Sub

Я понимаю, что реализация Disposable не соответствует текущим рекомендациям, но, надеюсь, вы все поняли идею.
Теперь, когда вызывается Dispose, сколько памяти освобождается?

Ответ: нет.
Вызов Dispose может освободить неуправляемые ресурсы, он НЕ МОЖЕТ вернуть управляемую память, это может сделать только GC. Это не значит, что вышеизложенное не является хорошей идеей, следуя приведенному выше шаблону, на самом деле все еще хорошая идея. После запуска Dispose ничто не мешает GC повторно запросить память, которая использовалась _Large, даже если экземпляр LargeStuff все еще находится в области видимости. Строки в _Large также могут относиться к поколению 0, но экземпляр LargeStuff может относиться к поколению 2, поэтому снова память будет востребована раньше.
Однако нет смысла добавлять финализатор для вызова метода Dispose, показанного выше. Это просто ЗАДЕРЖИТ повторное требование памяти, чтобы позволить финализатору работать.


1
Если экземпляр LargeStuffбыл достаточно длинным, чтобы попасть в Поколение 2, и если он _Largeсодержит ссылку на вновь созданную строку, которая находится в Поколении 0, то если экземпляр этого объекта LargeStuffбыл оставлен без обнуления _Large, тогда строка, на которую ссылается _Largeбудет храниться до следующей коллекции Gen2. Обнуление _Largeможет привести к удалению строки в следующей коллекции Gen0. В большинстве случаев обнуление ссылок бесполезно, но в некоторых случаях это может принести некоторую пользу.
суперкат

5

Помимо своего основного применения в качестве способа контролировать срок службы в системных ресурсах (полностью покрыта удивительным ответом Яна , престижность!), То IDisposable / с использованием комбы также может быть использован для определения области изменения состояния (критических) глобальных ресурсов : консоли , то потоки , то процесс , любой глобальный объект как экземпляр приложения .

Я написал статью об этом шаблоне: http://pragmateek.com/c-scope-your-global-state-changes-with-idisposable-and-the-using-statement/

Он иллюстрирует, как вы можете защитить некоторые часто используемые глобальные состояния многоразовым и читаемым способом: цвета консоли , текущую культуру потоков , свойства объекта приложения Excel ...


4

Во всяком случае, я ожидал бы, что код будет менее эффективным, чем когда он пропущен.

Вызывать методы Clear () не нужно, и GC, вероятно, не сделал бы этого, если бы Dispose этого не делал ...


2

Есть вещи, которые эта Dispose()операция делает в примере кода, которые могут иметь эффект, который не произойдет из-за обычного GC MyCollectionобъекта.

Если объекты, на которые ссылаются _theListили на _theDictкоторые ссылаются другие объекты, то этот объект List<>или Dictionary<>объект не будут подлежать сбору, но внезапно не будут иметь содержимого. Если бы не было операции Dispose (), как в примере, эти коллекции все равно содержали бы свое содержимое.

Конечно, если бы это была ситуация, я бы назвал это неправильным дизайном - я просто указываю (педантично, я полагаю), что Dispose()операция может быть не полностью избыточной, в зависимости от того, есть ли другие варианты использования List<>или Dictionary<>нет показано на фрагменте.


Это частные поля, поэтому я думаю, что было бы справедливо предположить, что OP не дает ссылки на них.
MQP

1) фрагмент кода является просто примером кода, поэтому я просто указываю, что может быть побочный эффект, который легко упустить из виду; 2) закрытые поля часто являются целью свойства / метода получателя - может быть, слишком много (некоторые люди считают, что получатель / установщик является своего рода анти-паттерном).
Майкл Берр

2

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

Вместо этого следует признать, что общего имеют все управляемые ресурсы: все они влекут за собой объект, требующий, чтобы какое-то внешнее «что-то» сделало что-то от его имени, в ущерб некоторым другим «вещам», а другой объект согласился сделать это до тех пор, пока дальнейшего уведомления. Если бы объект был оставлен и исчез без следа, ничто никогда не сообщило бы этой внешней «вещи», что ей больше не нужно изменять свое поведение от имени объекта, которого больше не существует; следовательно, полезность вещи будет постоянно уменьшаться.

Таким образом, неуправляемый ресурс представляет собой соглашение какой-то внешней «вещи» об изменении своего поведения от имени объекта, что бесполезно ухудшает полезность этой внешней «вещи», если объект был оставлен и прекратил существование. Управляемый ресурс - это объект, который является бенефициаром такого соглашения, но который подписался на получение уведомления об отказе от него и который будет использовать такое уведомление для наведения порядка в своих делах до его уничтожения.


Ну, ИМО, определение неуправляемого объекта понятно; любой не-GC объект .
Эонил

1
@Eonil: неуправляемый объект! = Неуправляемый ресурс. Такие вещи, как события, могут быть полностью реализованы с использованием управляемых объектов, но все же представляют собой неуправляемые ресурсы, потому что - по крайней мере в случае недолговечных объектов, подписывающихся на события долгоживущих объектов - GC ничего не знает о том, как их очистить ,
суперкат


2

Первое определение. Для меня неуправляемый ресурс означает некоторый класс, который реализует интерфейс IDisposable или что-то созданное с использованием вызовов в dll. GC не знает, как обращаться с такими объектами. Если класс имеет, например, только типы значений, я не рассматриваю этот класс как класс с неуправляемыми ресурсами. Для своего кода я следую следующим практикам:

  1. Если созданный мной класс использует некоторые неуправляемые ресурсы, это означает, что я должен также реализовать интерфейс IDisposable для очистки памяти.
  2. Очистите объекты, как только я закончу их использовать.
  3. В моем методе dispose я перебираю все IDisposable члены класса и вызываю Dispose.
  4. В моем методе Dispose вызовите GC.SuppressFinalize (this), чтобы уведомить сборщик мусора о том, что мой объект уже очищен. Я делаю это потому, что вызов GC - это дорогая операция.
  5. В качестве дополнительной меры предосторожности я пытаюсь сделать возможным вызов Dispose () несколько раз.
  6. Иногда я добавляю закрытый член _disposed и проверяю вызовы методов, когда объект был очищен. И если он был очищен, генерируйте ObjectDisposedException.
    Следующий шаблон демонстрирует то, что я описал словами как пример кода:

public class SomeClass : IDisposable
    {
        /// <summary>
        /// As usually I don't care was object disposed or not
        /// </summary>
        public void SomeMethod()
        {
            if (_disposed)
                throw new ObjectDisposedException("SomeClass instance been disposed");
        }

        public void Dispose()
        {
            Dispose(true);
        }

        private bool _disposed;

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
            if (disposing)//we are in the first call
            {
            }
            _disposed = true;
        }
    }

1
«Для меня неуправляемый ресурс означает некоторый класс, который реализует интерфейс IDisposable или что-то созданное с использованием вызовов в dll». То есть вы говорите, что любой тип, который is IDisposableсам по себе должен рассматриваться как неуправляемый ресурс? Это не кажется правильным. Также, если тип implmenting является чистым типом значения, вы, вероятно, предполагаете, что его не нужно удалять. Это также кажется неправильным.
Алуан Хаддад

Каждый судит сам. Я не люблю добавлять в мой код что-то просто ради дополнения. Это означает, что если я добавлю IDisposable, это означает, что я создал какую-то функциональность, которой GC не может управлять, или я полагаю, что он не сможет правильно управлять своим временем жизни.
Юрий Залецкий

2

Данный пример кода не является хорошим примером для IDisposableиспользования. Очистка словаря обычно не должна идти в Disposeметод. Элементы словаря будут очищены и удалены, когда они выйдут из области видимости.IDisposableреализация необходима для освобождения памяти / обработчиков, которые не будут освобождены / освобождены даже после того, как они выйдут из области видимости.

В следующем примере показан хороший пример шаблона IDisposable с некоторым кодом и комментариями.

public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                disposed = true;

            }
        }

        // Use interop to call the method necessary 
        // to clean up the unmanaged resource.
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}

1

Наиболее оправданный вариант использования для удаления управляемых ресурсов - это подготовка к GC для восстановления ресурсов, которые иначе никогда бы не были собраны.

Ярким примером являются круговые ссылки.

Хотя рекомендуется использовать шаблоны, которые избегают циклических ссылок, если вы в конечном итоге получите (например) «дочерний» объект, который имеет ссылку на свой «родительский», это может остановить сбор GC родительского объекта, если вы просто откажетесь ссылка и полагаться на GC - плюс, если вы реализовали финализатор, он никогда не будет вызываться.

Единственный способ обойти это - вручную разорвать циклические ссылки, установив для родительских ссылок значение null для дочерних элементов.

Реализация IDisposable на родителя и детей является лучшим способом сделать это. Когда Dispose вызывается для Parent, вызывается Dispose для всех дочерних элементов, а в дочернем методе Dispose установите для родительских ссылок значение null.


4
По большей части, GC не работает, идентифицируя мертвые объекты, а скорее идентифицируя живые. После того, как каждый цикл gc для каждого объекта, который был зарегистрирован для финализации, сохранен в куче больших объектов или является целью динамического объекта WeakReference, система проверит флаг, который указывает, что в последнем цикле GC была найдена прямая корневая ссылка и либо добавит объект в очередь объектов, нуждающихся в немедленной финализации, освободит объект из кучи больших объектов, либо сделает недействительной слабую ссылку. Циркулярные ссылки не будут поддерживать жизнь объектов, если другие ссылки не существуют.
суперкат

1

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

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

Для актуального вопроса; Если вы используете IDisposable для очистки управляемых объектов, которые занимают много памяти, короткий ответ будет отрицательным . Причина в том, что после удаления IDisposable вы должны позволить ему выйти из области видимости. На этом этапе любые дочерние объекты, на которые ссылаются, также находятся вне области и будут собраны.

Единственное реальное исключение - это если у вас много памяти, связанной в управляемых объектах, и вы заблокировали этот поток, ожидая завершения какой-либо операции. Если эти объекты не понадобятся после завершения этого вызова, то установка этих ссылок на null может позволить сборщику мусора быстрее их собрать. Но этот сценарий будет представлять плохой код, который необходимо реорганизовать, а не вариант использования IDisposable.


1
Я не понял, почему кто-то поставил -1 в твоем ответе
Себастьян Оскар Лопес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.