Должен ли IDisposable применяться каскадно?

Это довольно простой вопрос, однако я все еще немного борюсь с ним.

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

Когда у меня есть класс, который содержит DbConnection (реализует IDisposable), должен ли мой класс также реализовывать IDisposable и связывать вызов с DbConnection или любыми другими объектами IDisposable, которыми он владеет? В противном случае ресурсы DbConnections будут освобождены только тогда, когда мой класс будет GarbageCollected, тем самым отбрасывая его ссылку на соединение, и GC завершит DbConnection.

9 ответов

Решение

Да, вы ВСЕГДА внедряете IDisposable, если вы контролируете одноразовые объекты. ВСЕГДА. Ваш код не сломается, если вы этого не сделаете, но если вы этого не сделаете, он лишит вас цели иметь одноразовые предметы.

Общее правило оптимизации GC:

  • Любой класс, который управляет объектами, не управляемыми GC, должен реализовывать финализатор (и, как правило, должен также реализовывать IDisposable). Вот откуда обычно берутся одноразовые классы "верхнего уровня" - они обычно управляют HANDLE окном, сокетом, мьютексом или чем-то еще.
  • Любой класс, который создает экземпляр IDisposable, должен сам реализовать IDisposable и правильно утилизировать () его составляющие.
  • Любая функция, которая создает экземпляр объекта IDisposeable, должна должным образом использовать Dispose(), когда он будет использован. Не позволяйте этому просто выпасть из области видимости.

Эти правила могут быть согнуты или игнорироваться, если вы пишете приложение для себя, но при распространении кода для других вы должны быть профессионалом и соблюдать правила.

Логика здесь заключается в том, что когда вы управляете памятью вне представления GC, механизм GC не может должным образом управлять использованием вашей памяти. Например, в вашей куче.NET у вас может быть только 4-байтовый указатель, но в неуправляемой стране вы можете указать 200 МБ памяти. Движок GC не будет пытаться собрать их, пока у вас не будет нескольких десятков, потому что все, что он видит, - это несколько байтов; в то время как в реальном мире это выглядит как утечка памяти.

Следовательно, правило таково, что неуправляемая память должна освобождаться сразу же после ее использования (цепочка IDisposable сделает это за вас), в то время как управляемая память освобождается механизмом GC всякий раз, когда она обращается к нему.

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

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

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

Я написал подробное сообщение в блоге на эту конкретную тему - Инкапсуляция IDisposable ресурсов.

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

Если вы используете соединение с базой данных как локальную переменную в методе, вы можете использовать оператор using() {}.

using (SqlConnection sqlConnection = new SqlConnection(connStr))
{
...do stuff with connection here
}

Оператор using() {} автоматически вызывает Dispose() для объектов, объявленных в (). (Также требуется, чтобы объекты, объявленные в (), реализовали IDisposable, чтобы гарантировать, что они могут быть удалены)

Если вы вместо этого работаете с DbConnection как частной переменной, которая инициализируется во время создания объекта или каким-либо другим методом инициализации, то вы, вероятно, захотите реализовать IDisposable самостоятельно, а затем вызвать _dbConnection.Dispose() в своем методе Dispose(). Таким образом, когда ваш объект будет удален, объект соединения с БД также будет расположен.

public class MyDALObj : IDisposable
{

public MyDalObj()
{
... create _dbConn object ...
}

public void Dispose()
{
_dbConn.Dispose();
}

private DbConnection _dbConn;
}

Есть два разных сценария:

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

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

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

В первом случае вам необходимо определиться со следующими тремя решениями:

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

Это было много текста, поэтому позвольте мне подвести итог:

  1. Объекты, которыми вы владеете, нужно утилизировать.
  2. Объекты вы не делаете, вы не распоряжаетесь.

Есть также третий случай, который звучит не так, как у вас, но тем не менее.

В случае, когда вы создаете, используете и отбрасываете объект локально, внутри одного метода, не передавая его и не сохраняя его в полях класса, вы используете using утверждение вместо этого, как это:

using (IDbConnection conn = ....())
{
}

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

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

Поскольку мы никогда не знаем, когда объект будет собираться GC, мы используем интерфейс IDisposable, чтобы иметь возможность преднамеренно освободить неуправляемые ресурсы до того, как объект будет собран мусором. Если одноразовый объект не утилизируется до сбора, его ресурсы могут быть не освобождены до тех пор, пока не завершится работа домена приложения. Это почти неписанное правило, согласно которому каждый объект, имеющий ссылку на объект IDisposable, сам должен быть IDisposable и вызывать метод Dispose своих ссылок IDisposable в своем собственном методе Dispose.

Это, безусловно, лучшая практика, особенно при работе с тяжелыми / неуправляемыми объектами.

Изменить: Лучшая практика, но не обязательно.

Конечно, вы можете устранить большую часть (пере) стоимости реализации IDisposable и получить что-то очень близкое к детерминированной финализации объектов в управляемой куче, если вы используете C++/CLI. Это часто (я считаю) упускаемый из вида аспект языка, который многие люди, похоже, отправляют в корзину "только для клея".

Другие вопросы по тегам