Перейти к содержимому

Dispose c что делает

  • автор:

Различие Dispose , Finalize, Деструктора и класса GC

Выходит, что деструктор и финализатор это одно и то же ? А срабатывает ли конструкция using в объекте, где не реализовывают IDisposable ?

6 мар 2016 в 17:41

@PolyakovSergey русским же языком написал: нет, не одно и тоже. Даже написал, в чем разница. Читайте внимательнее.

6 мар 2016 в 17:42
GС руками вообще лучше не трогать, без крайней необходимости.
6 мар 2016 в 17:49

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

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

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

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

Реализация метода Dispose

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

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

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

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

Что касается внедрения зависимостей, то при регистрации служб в IServiceCollectionслужбах время существования службы управляется неявно от вашего имени. И IServiceProvider соответствующие IHost функции оркестрации очистки ресурсов. В частности, реализации IDisposable и IAsyncDisposable должным образом удаляются в конце указанного времени существования.

Дополнительные сведения см. в статье Внедрение зависимостей в .NET.

Безопасные дескрипторы

Написание кода для метода завершения объекта является сложной задачей, которая может вызвать проблемы при неправильном выполнении. Поэтому вместо реализации метода завершения рекомендуется создавать объекты System.Runtime.InteropServices.SafeHandle.

System.Runtime.InteropServices.SafeHandle — это абстрактный управляемый тип, выполняющий роль оболочки для System.IntPtr, который идентифицирует неуправляемый ресурс. В Windows он может идентифицировать дескриптор, а в Unix — дескриптор файла. предоставляет SafeHandle всю логику, необходимую для обеспечения того, чтобы этот ресурс был освобожден один раз и только один раз, когда SafeHandle удаляется или когда все ссылки на SafeHandle были удалены и SafeHandle экземпляр завершен.

System.Runtime.InteropServices.SafeHandle — это абстрактный базовый класс. Производные классы предоставляют определенные экземпляры для различных видов дескрипторов. Эти производные классы проверяют, какие значения System.IntPtr считаются недопустимыми и как фактически освободить дескриптор. Например, класс SafeFileHandle является производным от SafeHandle , выступает оболочкой для структур IntPtrs , которые определяют открытые дескрипторы файлов, а также переопределяет свой метод SafeHandle.ReleaseHandle() для его закрытия (через функцию close в UNIX или CloseHandle в Windows). Большинство API-интерфейсов в библиотеках .NET, создающих неуправляемый SafeHandle ресурс, заключает его в и при необходимости возвращает SafeHandle его вам, а не возвращает необработанный указатель. В ситуациях, когда вы взаимодействуете с неуправляемым компонентом и получаете структуру IntPtr для неуправляемого ресурса, можно создать собственный тип SafeHandle в качестве оболочки структуры. В результате для реализации методов завершения требуется несколько типов, отличных SafeHandle от типов. Большинство реализаций одноразовых шаблонов в конечном итоге только упаковывают другие управляемые ресурсы, некоторые из которых могут быть объектами SafeHandle .

Следующие производные Microsoft.Win32.SafeHandles классы в пространстве имен предоставляют безопасные дескрипторы.

Класс Ресурсы, которые она хранит
SafeFileHandle
SafeMemoryMappedFileHandle
SafePipeHandle
Файлы, сопоставленные в памяти файлы и каналы
SafeMemoryMappedViewHandle представления памяти;
SafeNCryptKeyHandle
SafeNCryptProviderHandle
SafeNCryptSecretHandle
конструкции шифрования;
SafeRegistryHandle Разделы реестра
SafeWaitHandle дескрипторы ожидания.

Dispose() и Dispose(bool)

Интерфейс IDisposable требует реализации одного метода Dispose без параметров. Кроме того, любой непечатанный класс должен иметь метод перегрузки Dispose(bool) .

  • public non-virtual ( NotOverridable в Visual Basic) (IDisposable.Dispose реализация).
  • protected virtual ( Overridable в Visual Basic) Dispose(bool) .

Метод Dispose()

public Так как метод , не виртуальный ( NotOverridable в Visual Basic), метод без Dispose параметров вызывается, когда он больше не нужен (потребителю типа), его целью является освобождение неуправляемых ресурсов, выполнение общей очистки и указание на то, что метод завершения, если он существует, не должен выполняться. Освобождение физической памяти, связанной с управляемым объектом, всегда оставляется сборщику мусора. Он имеет стандартную реализацию:

public void Dispose() < // Dispose of unmanaged resources. Dispose(true); // Suppress finalization. GC.SuppressFinalize(this); >
Public Sub Dispose() _ Implements IDisposable.Dispose ' Dispose of unmanaged resources. Dispose(True) ' Suppress finalization. GC.SuppressFinalize(Me) End Sub 

Метод Dispose полностью выполняет очистку объектов, поэтому сборщику мусора не требуется вызывать переопределенный метод Object.Finalize. Таким образом, вызов метода SuppressFinalize не позволит сборщику мусора запустить метод завершения. Если тип не имеет метода завершения, вызов метода GC.SuppressFinalize не производит эффекта. Фактическая очистка выполняется перегрузкой Dispose(bool) метода.

Перегрузка метода Dispose(Boolean)

В этой перегрузке параметр disposing типа Boolean указывает, откуда осуществляется вызов метода: из метода Dispose (значение true ) или из метода завершения (значение false ).

protected virtual void Dispose(bool disposing) < if (_disposed) < return; >if (disposing) < // TODO: dispose managed state (managed objects). >// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. _disposed = true; > 
Protected Overridable Sub Dispose(disposing As Boolean) If disposed Then Exit Sub ' A block that frees unmanaged resources. If disposing Then ' Deterministic call… ' A conditional block that frees managed resources. End If disposed = True End Sub 

Параметр disposing при вызове из метода завершения должен иметь значение false , а при вызове из метода IDisposable.Dispose — значение true . Иными словами, при детерминированном вызове он будет иметь значение true , а при недетерминированном вызове — false .

Тело метода состоит из трех блоков кода:

  • Блок для условного возврата, если объект уже удален.
  • Блок, который освобождает неуправляемые ресурсы. Этот блок выполняется вне зависимости от значения параметра disposing .
  • Условный блок, который освобождает управляемые ресурсы. Этот блок выполняется, если параметр disposing имеет значение true . К управляемым ресурсам, которые он освобождает, могут относиться:
    • Управляемые объекты, реализующие IDisposable. Условный блок может использоваться для вызова реализации Dispose (каскадное удаление). При использовании класса, производного от System.Runtime.InteropServices.SafeHandle, в качестве оболочки для неуправляемого ресурса необходимо вызвать реализацию SafeHandle.Dispose().
    • Управляемые объекты, которые используют большие объемы памяти или дефицитные ресурсы. Назначайте ссылки на большие управляемые объекты в null , чтобы они чаще оказывались недоступными. Это освобождает их быстрее, чем если бы они были освобождены недетерминированным образом.

    Если метод вызывается из метода завершения, должен выполняться только тот код, который освобождает неуправляемые ресурсы. Разработчик отвечает за то, чтобы ложный путь не взаимодействовал с управляемыми объектами, которые могли быть удалены. Это важно, так как порядок, в котором сборщик мусора удаляет управляемые объекты во время завершения, не детерминирован.

    Каскадные вызовы Dispose

    Если класс владеет полем или свойством и его тип реализует IDisposable, содержащий класс должен также реализовывать IDisposable. Класс, который создает IDisposable экземпляр реализации и сохраняет ее в качестве члена экземпляра, также отвечает за ее очистку. Это помогает гарантировать, что ссылочные удаляемые типы получают возможность детерминированного выполнения очистки Dispose с помощью метода . В следующем примере используется sealed класс (или NotInheritable в Visual Basic).

    using System; public sealed class Foo : IDisposable < private readonly IDisposable _bar; public Foo() < _bar = new Bar(); >public void Dispose() => _bar.Dispose(); > 
    Public NotInheritable Class Foo Implements IDisposable Private ReadOnly _bar As IDisposable Public Sub New() _bar = New Bar() End Sub Public Sub Dispose() Implements IDisposable.Dispose _bar.Dispose() End Sub End Class 
    • Если класс имеет IDisposable поле или свойство, но не владеет им, то есть класс не создает объект , то классу не нужно реализовывать IDisposable.
    • Бывают случаи, когда может потребоваться выполнить null проверку в методе завершения (который включает Dispose(false) метод, вызываемый методом завершения). Одна из основных причин заключается в том, что вы не уверены, был ли экземпляр полностью инициализирован (например, в конструкторе может быть вызвано исключение).

    Реализация шаблона освобождения

    Все непечатанные классы (или классы Visual Basic, не измененные как ) следует рассматривать как NotInheritable потенциальные базовые классы, так как они могут быть унаследованы. При реализации шаблона освобождения для любого класса, который может быть базовым, необходимо обеспечить следующее:

    • Реализация Dispose, которая вызывает метод Dispose(bool) .
    • Метод Dispose(bool) , который выполняет фактическую очистку.
    • Любой класс, производный от класса SafeHandle, который создает оболочку для неуправляемого ресурс (рекомендуется), или переопределенный метод Object.Finalize. Класс SafeHandle предоставляет метод завершения, поэтому вам не нужно писать его самостоятельно.

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

    Ниже приведен общий пример реализации шаблона удаления для базового класса, использующего безопасный дескриптор.

    using Microsoft.Win32.SafeHandles; using System; using System.Runtime.InteropServices; public class BaseClassWithSafeHandle : IDisposable < // To detect redundant calls private bool _disposedValue; // Instantiate a SafeHandle instance. private SafeHandle? _safeHandle = new SafeFileHandle(IntPtr.Zero, true); // Public implementation of Dispose pattern callable by consumers. public void Dispose() < Dispose(true); GC.SuppressFinalize(this); >// Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) < if (!_disposedValue) < if (disposing) < _safeHandle?.Dispose(); _safeHandle = null; >_disposedValue = true; > > > 
    Imports Microsoft.Win32.SafeHandles Imports System.Runtime.InteropServices Public Class BaseClassWithSafeHandle Implements IDisposable ' To detect redundant calls Private _disposedValue As Boolean ' Instantiate a SafeHandle instance. Private _safeHandle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True) ' Public implementation of Dispose pattern callable by consumers. Public Sub Dispose() _ Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub ' Protected implementation of Dispose pattern. Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not _disposedValue Then If disposing Then _safeHandle?.Dispose() _safeHandle = Nothing End If _disposedValue = True End If End Sub End Class 

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

    Вот общий шаблон реализации шаблона удаления для базового класса, который переопределяет метод Object.Finalize.

    using System; public class BaseClassWithFinalizer : IDisposable < // To detect redundant calls private bool _disposedValue; ~BaseClassWithFinalizer() =>Dispose(false); // Public implementation of Dispose pattern callable by consumers. public void Dispose() < Dispose(true); GC.SuppressFinalize(this); >// Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) < if (!_disposedValue) < if (disposing) < // TODO: dispose managed state (managed objects) >// TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null _disposedValue = true; > > > 
    Public Class BaseClassWithFinalizer Implements IDisposable ' To detect redundant calls Private _disposedValue As Boolean Protected Overrides Sub Finalize() Dispose(False) End Sub ' Public implementation of Dispose pattern callable by consumers. Public Sub Dispose() _ Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub ' Protected implementation of Dispose pattern. Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not _disposedValue Then If disposing Then ' TODO: dispose managed state (managed objects) End If ' TODO free unmanaged resources (unmanaged objects) And override finalizer ' TODO: set large fields to null _disposedValue = True End If End Sub End Class 

    В C# вы реализуете завершение путем предоставления метода завершения, а не путем переопределения Object.Finalize. В Visual Basic вы создаете метод завершения с Protected Overrides Sub Finalize() помощью .

    Реализация шаблона освобождения для производного класса

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

    • Метод protected override void Dispose(bool) , который переопределяет метод базового класса и выполняет фактическую очистку производного класса. Этот метод также должен вызывать base.Dispose(bool) метод ( MyBase.Dispose(bool) в Visual Basic), передавая ему состояние удаления ( bool disposing параметр) в качестве аргумента.
    • Любой класс, производный от класса SafeHandle, который создает оболочку для неуправляемого ресурс (рекомендуется), или переопределенный метод Object.Finalize. Класс SafeHandle содержит метод завершения, что освобождает разработчика от необходимости создавать его вручную. Если вы предоставляете метод завершения, он должен вызвать перегрузку Dispose(bool) с false аргументом .

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

    using Microsoft.Win32.SafeHandles; using System; using System.Runtime.InteropServices; public class DerivedClassWithSafeHandle : BaseClassWithSafeHandle < // To detect redundant calls private bool _disposedValue; // Instantiate a SafeHandle instance. private SafeHandle? _safeHandle = new SafeFileHandle(IntPtr.Zero, true); // Protected implementation of Dispose pattern. protected override void Dispose(bool disposing) < if (!_disposedValue) < if (disposing) < _safeHandle?.Dispose(); _safeHandle = null; >_disposedValue = true; > // Call base class implementation. base.Dispose(disposing); > > 
    Imports Microsoft.Win32.SafeHandles Imports System.Runtime.InteropServices Public Class DerivedClassWithSafeHandle Inherits BaseClassWithSafeHandle ' To detect redundant calls Private _disposedValue As Boolean ' Instantiate a SafeHandle instance. Private _safeHandle As SafeHandle = New SafeFileHandle(IntPtr.Zero, True) Protected Overrides Sub Dispose(ByVal disposing As Boolean) If Not _disposedValue Then If disposing Then _safeHandle?.Dispose() _safeHandle = Nothing End If _disposedValue = True End If ' Call base class implementation. MyBase.Dispose(disposing) End Sub End Class 

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

    Вот общий шаблон реализации шаблона удаления для производного класса, который переопределяет метод Object.Finalize:

    public class DerivedClassWithFinalizer : BaseClassWithFinalizer < // To detect redundant calls private bool _disposedValue; ~DerivedClassWithFinalizer() =>Dispose(false); // Protected implementation of Dispose pattern. protected override void Dispose(bool disposing) < if (!_disposedValue) < if (disposing) < // TODO: dispose managed state (managed objects). >// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. _disposedValue = true; > // Call the base class implementation. base.Dispose(disposing); > > 
    Public Class DerivedClassWithFinalizer Inherits BaseClassWithFinalizer ' To detect redundant calls Private _disposedValue As Boolean Protected Overrides Sub Finalize() Dispose(False) End Sub ' Protected implementation of Dispose pattern. Protected Overrides Sub Dispose(ByVal disposing As Boolean) If Not _disposedValue Then If disposing Then ' TODO: dispose managed state (managed objects). End If ' TODO free unmanaged resources (unmanaged objects) And override a finalizer below. ' TODO: set large fields to null. _disposedValue = True End If ' Call the base class implementation. MyBase.Dispose(disposing) End Sub End Class 

    См. также раздел

    • Удаление служб
    • SuppressFinalize
    • IDisposable
    • IDisposable.Dispose
    • Microsoft.Win32.SafeHandles
    • System.Runtime.InteropServices.SafeHandle
    • Object.Finalize
    • Определение и использование классов и структур (C++/CLI)

    Совместная работа с нами на GitHub

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

    Dispose pattern

    Ни для кого не будет секретом, что платформа .NET поддерживает автоматическое управление памятью. Это значит, что если вы создадите объект с помощью ключевого слова new, то вам не нужно будет самостоятельно заботиться о его освобождении. Сборщик мусора определит «достижимость» объекта, и если на объект не осталось корневых ссылок, то он будет освобожден. Однако, как только речь заходит о ресурсах, таких как сокет, буфер неуправляемой памяти, дескриптор операционной системы и т.д., то сборщик мусора, по большому счету, умывает руки и весь головняк по работе с такими ресурсами ложится на плечи разработчика.

    А как же финализаторы? – спросите вы. Ну, да, есть такое дело, финализаторы действительно предназначены для освобождения ресурсов, но проблема в том, что время их вызова не детерминировано, а это значит, что никто не знает, когда они будут вызваны и будут ли вызваны вообще. Да и порядок вызова финализаторов не определен, поэтому при вызове финализатора некоторые «части» вашего объекта уже могут быть «разрушены», поскольку их финализаторы уже были вызваны. В общем, финализаторы – они-то есть, но это скорее «страховочный трос», а не нормальное средство управления ресурсами.

    Идиома RAII

    В языке С++, в котором нет никаких встроенных средств для автоматического управления памятью помимо умных указателей, уже давно активно применяется паттерн (или идиома) для своевременного освобождения ресурсов (*). Эта идиома носит название «Захват ресурса есть инициализация» (RAII — Resource Acquisition Is Initialization) и заключается в следующем. Ресурс захватывается в конструкторе и освобождается в деструкторе, а поскольку деструкторы вызываются автоматически, то и дополнительных усилий по управлению ресурсами больше не требуется.

    Не удивительно, что эта же идея детерминированного управления ресурсами перекачивала и в другие более «умные» и «управляемые» среды, такие как .NET или Java (**) в виде интерфейса IDisposable (в языке C#) и метода dispose (в Java). Но, поскольку эти среды более умные, по сравнению со старичком С++, и основные проблемы, связанные с управлением памятью, в них решены, то переехала эта идиома не слишком хорошо. Нет, поймите меня правильно, переехала она вполне успешно, но для этого вам нужно использовать блок using (для языка C#) или trywithresourcesstatement (в Java 7), если же вы «забудете» ими воспользоваться, то от вашего детерминированного освобождения ресурсов не останется и следа.

    // Открываем файл внутри блока using
    using ( FileStream file = File .OpenRead( «foo.txt» ))
    // Выходим из функции при выполнении некоторого условия
    if (someCondition) return ;
    // Файл будет закрыт автоматически при выходе из блока using
    >
    // А что, если кто-то откроет файла вне блока using?
    FileStream file2 = File .OpenRead( «foo.txt» );

    * This source code was highlighted with Source Code Highlighter .

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

    Все это привело к тому, что относительно простая идиома RAII вылилась на платформе .NET в паттерн, который так и называется “Dispose паттерн”. Однако прежде чем переходить к его рассмотрению, давайте рассмотрим два типа ресурсов, существующие на платформе .NET: управляемые и неуправляемые ресурсы.

    Управляемые и неуправляемые ресурсы

    В .NET существует два типа ресурсов: управляемые и неуправляемые. Причем отличить их довольно просто: к неуправляемым ресурсам относятся только «сырые» ресурсы, типа IntPtr, сырые дескрипторы сокетов или что-то в этом духе; если же с помощью идиомы RAII этот ресурс упаковали в объект, захватывающий его в конструкторе и освобождающий в методе Dispose, то такой ресурс уже является управляемым. По сути, управляемые ресурсы – это «умные оболочки» для неуправляемых ресурсов, для освобождения которых не нужно вызывать какие-то хитроумные функции, а достаточно вызвать метод Dispose интерфейса IDisposable.

    class NativeResourceWrapper : IDisposable
    // IntPtr – описатель неуправляемого ресурса
    private IntPtr nativeResourceHandle;
    public NativeResourceWrapper()
    // «Захватываем» неуправляемый ресурс путем вызова специальной функции
    nativeResourceHandle = AcquireNativeResource();
    >
    public void Dispose()
    // Освобождаем захваченный ресурс, опять же, путем вызова какой-то
    // специальной функции
    ReleaseNativeResource(nativeResourceHandle);
    >
    // Есть еще и финализатор, но его роль будет раскрыта позднее
    ~NativeResourceWrapper() <. >
    >

    * This source code was highlighted with Source Code Highlighter .

    Таким образом, любой объект может владеть ресурсами двух типов: он может непосредственно содержать неуправляемый ресурс (например, IntPtr) или же он может содержать ссылку на управляемый ресурс (например, NativeResourceWrapper), при этом в обоих случаях объект, содержащий один из этих ресурсов, сам становится управляемым ресурсом. Это может показаться не слишком принципиальным, но понимать разницу между двумя типами ресурсов очень важно, поскольку работать с ними приходится по-разному.

    Dispose pattern

    Итак, мы знаем, что объект может владеть двумя типами ресурсов: управляемыми и неуправляемыми; а также то, что у нас есть два способа освобождения ресурсов: детерминированный, с помощью метода Dispose и недетерминированный, с помощью финализатора (***). А теперь давайте посмотрим, как со всем этим добром жить и, главное, как это добро освобождать.

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

    1. Класс, содержащий управляемые или неуправляемые ресурсы реализует интерфейс IDisposable

    class Boo : IDisposable

    * This source code was highlighted with Source Code Highlighter .

    2. Класс содержит метод Dispose(booldisposing), который и делает всю работу по освобождению ресурсов; параметр disposing говорит о том, вызывается ли этот метод из метода Dispose или из финализатора. Этот метод должен быть protectedvirtual для не-sealed классов и private для sealed классов

    // Для не-sealed классов
    protected virtual void Dispose( bool disposing) <>

    // Для sealed классов
    private void Dispose( bool disposing) <>

    * This source code was highlighted with Source Code Highlighter .

    3. Метод Dispose всегда реализуется следующим образом: вначале вызывается метод Dispose(true), а затем может следовать вызов метода GC.SuppressFinalize(), который предотвращает вызов финализатора.

    public void Dispose()
    Dispose( true /*called by user directly*/ );
    GC.SuppressFinalize( this );
    >

    * This source code was highlighted with Source Code Highlighter .

    Метод GC.SuppressFinalize(), во-первых, должен вызываться после вызова Dispose(true), а не перед ним, поскольку если метод Dispose(true) «упадет» с исключением, то выполнение финализатора не отменится. Во-вторых, GC.SuppressFinalize() должен вызываться даже для классов, не содержащих финализаторы, поскольку финализатор может появиться у его наследника (т.е. мы должны вызывать метод GC.SuppressFinalize() во всех не-sealed классах).

    4. Метод Dispose(booldisposing) содержит две части: (1) если этот метод вызван из метода Dispose (т.е. параметр disposing равен true), то мы освобождаем управляемые и неуправляемые ресурсы и (2) если метод вызван из финализатора во время сборки мусора (параметр disposing равен false), то мы освобождаем только неуправляемые ресурсы.

    void Dispose( bool disposing)
    if (disposing)
    // Освобождаем только управляемые ресурсы
    >

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

    * This source code was highlighted with Source Code Highlighter .

    5. (ОПЦИОНАЛЬНО) Класс может содержать финализатор и вызывать из него Dispose(booldisposing) передавая false в качестве параметра.

    ~Boo()
    Dispose( false /*not called by user directly*/ );
    >

    * This source code was highlighted with Source Code Highlighter .

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

    6. (ОПЦИОНАЛЬНО) Класс может содержать поле bool _disposed, которое говорит о том, что ресурсы объекта уже освобождены. Disposable-классы должны спокойно позволять повторный вызов метода Dispose, а также генерировать исключение при доступе к любым другим публичным методам или свойствам (поскольку инвариант объекта уже разрушен).

    void Dispose( bool disposing)
    if (disposed)
    return ; // Ресурсы уже освобождены
    // Освобождаем ресурсы
    disposed = true ;
    >

    public void SomeMethod()
    if (disposed)
    throw new ObjectDisposedException();
    >

    * This source code was highlighted with Source Code Highlighter .

    7. (ОПЦИОНАЛЬНО) Класс может наследовать от CriticalFinalizerObject, если предыдущих шести пунктов мало и вы хотите большей экзотики. Наследование от этого класса дает вам дополнительные гарантии:

    1. Финализатор таких классов компилируется JIT-компилятором сразу при конструировании экземпляра, а не отложено по мере необходимости. Это дает возможность успешно выполниться финализатору даже в случае острой нехватки памяти.
    2. Как мы уже говорили, CLR не гарантирует порядок вызова финализаторов, что делает невозможным обращение внутри финализатора к другим объектам, содержащим неуправляемые ресурсы. Однако CLR гарантирует что финализаторы «простых смертных» объектов будут вызваны до наследников CriticalFinalizerObject. Это дает возможность, в частности, из финализаторов ваших классов (если они не наследуют от CriticalFinalizerObject) обращаться к полю SafeHandle, которое точно будет освобождено позднее.
    3. Финализаторы таких классов будут вызваны даже в случае экстренной выгрузки домена приложения.

    // А вы уверены, что оно вам нужно?
    class Foo : CriticalFinalizerObject <>

    * This source code was highlighted with Source Code Highlighter .

    Прагматичный взгляд на Dispose паттерн

    Если вам показалось, что работа с ресурсами в .NET неоправдано сложна, то у меня по этому поводу есть две новости: одна хорошая, а другая – не очень. Новость «не очень» заключается в том, что работа с ресурсами даже сложнее, чем здесь описано (*****), хорошая же заключается в том, что в большинстве случаев вся эта сложность нас с вами касаться практически не будет.

    Вся сложности реализации Dispose паттерна связаны с предположением о том, что один и тот же класс (или иерархия классов) может одновременно содержать как управляемые, так и неуправляемые ресурсы. Но давайте подумаем, а зачем вообще нам может понадобиться хранить неуправляемые ресурсы напрямую в классах бизнес-логики? А как же пресловутые Принципы Единой Ответственности (SRP – Single Responsibility Principle) и Здравого Смысла? Идиома RAII, описанная ранее, успешно используется десятки лет и предназначена как раз для таких случаев: если у вас есть неуправляемый ресурс, то вместо того, чтобы работать с ним напрямую, оберните его в управляемую оболочку и работайте уже с нею.

    Если посмотреть на .NET Framework, то можно заметить, что там используется именно такой подход: для всех ресурсов создается оболочка, которая прячет внутри всю сложность по работе с ресурсами, предоставляя пользователю лишь вызвать метод Dispose для явной очистки ресурсов (ну, и финализатор, на всякий случай). Кроме того, для большей части неуправляемых ресурсов операционной системы такие оболочки уже сделаны, и изобретать велосипед не нужно.

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

    Упрощенная версия Dispose паттерна

    Если мы с вами знаем, что ни один человек не собирается смешивать управляемые и неуправляемые ресурсы в одном месте, так почему бы не реализовать это в коде явным образом? Мы можем оставить метод Dispose и вместо дополнительного метода Dispose с совсем невнятным булевым параметром, добавить виртуальный метод DisposeManagedResources, имя которого будет четко говорить о том, что мы должны освободить именно управляемые ресурсы. Модификатор доступа этого метода должен быть аналогичным нашему методу Dispose(bool), т.е. protected virtual для не-sealed классов или private для sealed классов.

    class SomethingWithManagedResources : IDisposable
    public void Dispose()
    // Никаких Dispose(true) и никаких вызовов GC.SuppressFinalize()
    DisposeManagedResources();
    >

    // Никаких параметров, этот метод должен освобождать только неуправляемые ресурсы
    protected virtual void DisposeManagedResources() <>
    >

    * This source code was highlighted with Source Code Highlighter .

    С первого взгляда такой подход может показаться слишком уж прагматичным, однако посудите сами: в книге Framework Design Guidelines описанию Dispose паттерна посвящено два десятка страниц, при этом ее авторы рекомендуют добавлять финализаторы только в случае острой необходимости. При этом все мы знаем, что смешивать два типа ресурсов в одном классе плохо, но все же следуем паттерну, который это поощряет, а не запрещает.

    Заключение

    При разработке библиотечных классов или бизнес-классов, которые будут использоваться в десятке других проектов, то следовать всем описанным выше принципам вполне разумно. К повторно используемому коду предъявляются другие требования и при их проектировании нужно следовать другим принципам: простота в использовании и расширяемость таких классов значительно важнее стоимости сопровождения.

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

    (*) В С++, в отличие от C#, память тоже является ресурсом. Поэтому идиома RAII в языке С++ применяется как для освобождения динамически выделенной памяти, так и для освобождения любых других ресурсов, типа дескрипторов ОС или сокетов.

    (**) В Java 7 наконец-то появилась конструкция, аналогичная конструкции using языка C#: try-with-resource statement

    (***) К сожалению в языке C# для финализаторов выбран тот же самый синтаксис (тильда, за которой идет имя класса), который используется для деструкторов в языке С++. Но семантика деструктора и финализатора очень разная, поскольку деструктор подразумевает детерминированное освобождение ресурсов, а финализатор – нет.

    (***) Да, это еще одно отличие в поведении .NET и языка С++. В последнем, деструктор вызывается только для полностью сконструированного объекта, при этом вызываются деструкторы для всех полностью сконструированных его полей.

    (****) Здесь я, например, не говорил о том, как можно получить «утечку ресурсов» при появлении исключений или о проблемах с изменяемыми значимыми типами, реализующими интерфейс IDisposable. Об этом я уже писал ранее в заметках «Гарантии безопасности исключений» и «О вреде изменяемых значимых типов» соответственно.

    • .net
    • patterns and practices
    • IDisposable

    Dispose c что делает

    Большинство объектов, используемых в программах на C#, относятся к управляемым или managed-коду. Такие объекты управляются CLR и легко очищаются сборщиком мусора. Однако вместе с тем встречаются также и такие объекты, которые задействуют неуправляемые объекты (подключения к файлам, базам данных, сетевые подключения и т.д.). Такие неуправляемые объекты обращаются к API операционной системы. Сборщик мусора может справиться с управляемыми объектами, однако он не знает, как удалять неуправляемые объекты. В этом случае разработчик должен сам реализовывать механизмы очистки на уровне программного кода.

    Освобождение неуправляемых ресурсов подразумевает реализацию одного из двух механизмов:

    • Создание деструктора
    • Реализация классом интерфейса System.IDisposable

    Создание деструкторов

    Если вы вдруг программировали на языке C++, то наверное уже знакомы с концепцией деструкторов. Метод деструктора носит имя класса (как и конструктор), перед которым стоит знак тильды ( ~ ).

    Деструкторы можно определить только в классах. Деструктор в отличие от конструктора не может иметь модификаторов доступа и параметры. При этом каждый класс может иметь только один деструктор.

    Например, определим в классе Person простейший деструктор:

    class Person < public string Name < get;>public Person(string name) => Name = name; ~Person() < Console.WriteLine($"has deleted"); > >

    В данном случае в деструкторе в целях демонстрации просто выводится строка на консоль, которая уведомляет, что объект удален. Но в реальных программах в деструктор вкладывается логика освобождения неуправляемых ресурсов.

    Однако на деле при очистке сборщик мусора вызывает не деструктор, а метод Finalize . Все потому, что компилятор C# компилирует деструктор в конструкцию, которая эквивалентна следующей:

    protected override void Finalize() < try < // здесь идут инструкции деструктора >finally < base.Finalize(); >>

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

    Используя в программе класс Person, после ее завершения можно будет увидеть на консоли сообщение об удалении объекта tom:

    Test(); GC.Collect(); // очистка памяти под объект tom Console.Read(); // ставим задержку void Test() < Person tom = new Person("Tom"); >public class Person < public string Name < get;>public Person(string name) => Name = name; ~Person() < Console.WriteLine($"has been deleted"); > >

    Обратите внимание, что даже после завершения метода Test и соответственно удаления из стека ссылки на объект Person в куче, может не последовать немедленного вызова деструктора. Лишь при завершении всей программы гарантировано произойдет очистка памяти. Однако с .NET 5 и в последующих версиях при завершении программы деструкторы не вызываются. Поэтому в программе выше для более быстрой очистки памяти применяется метод GC.Collect и для гарантированного вызова деструктора устанавливается задержка с помощью вызова Console.Read() , который ожидает от пользователя ввода.

    На уровне памяти это выглядит так: сборщик мусора при размещении объекта в куче определяет, поддерживает ли данный объект метод Finalize . И если объект имеет метод Finalize, то указатель на него сохраняется в специальной таблице, которая называется очередь финализации. Когда наступает момент сборки мусора, сборщик видит, что данный объект должен быть уничтожен, и если он имеет метод Finalize, то он копируется в еще одну таблицу и окончательно уничтожается лишь при следующем проходе сборщика мусора.

    Стоит отметить, что точное время вызова деструктора не определено. Кроме того, при финализации двух связанных объектов порядок вызова деструкторов не гарантируется. То есть если объект A хранит ссылку на объект B, и при этом оба эти объекта имеют деструкторы, то для объекта B деструктор моет уже отработать в то время, как для объекта A деструктор только начнет работу.

    И здесь мы можем столкнуться со следующей проблемой: а что если нам немедленно надо вызвать деструктор и освободить все связанные с объектом неуправляемые ресурсы? В этом случае мы можем использовать второй подход — реализацию интерфейса IDisposable.

    Интерфейс IDisposable

    Интерфейс IDisposable объявляет один единственный метод Dispose , в котором при реализации интерфейса в классе должно происходить освобождение неуправляемых ресурсов. Например:

    Test(); void Test() < Person? tom = null; try < tom = new Person("Tom"); >finally < tom?.Dispose(); >> public class Person : IDisposable < public string Name < get;>public Person(string name) => Name = name; public void Dispose() < Console.WriteLine($"has been disposed"); > >

    В данном коде используется конструкция try. finally. По сути эта конструкция по функционалу в общем эквивалентна следующим двум строкам кода:

    Person tom = new Person("Tom"); tom.Dispose();

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

    Комбинирование подходов

    Мы рассмотрели два подхода. Какой же из них лучше? С одной стороны, метод Dispose позволяет в любой момент времени вызвать освобождение связанных ресурсов, а с другой — программист, использующий наш класс, может забыть поставить в коде вызов метода Dispose. В общем бывают различные ситуации. И чтобы сочетать плюсы обоих подходов мы можем использовать комбинированный подход. Microsoft предлагает нам использовать следующий формализованный шаблон:

    public class SomeClass: IDisposable < private bool disposed = false; // реализация интерфейса IDisposable. public void Dispose() < // освобождаем неуправляемые ресурсы Dispose(true); // подавляем финализацию GC.SuppressFinalize(this); >protected virtual void Dispose(bool disposing) < if (disposed) return; if (disposing) < // Освобождаем управляемые ресурсы >// освобождаем неуправляемые объекты disposed = true; > // Деструктор ~SomeClass() < Dispose (false); >>

    Логика очистки реализуется перегруженной версией метода Dispose(bool disposing) . Если параметр disposing имеет значение true, то данный метод вызывается из публичного метода Dispose, если false — то из деструктора.

    При вызове деструктора в качестве параметра disposing передается значение false, чтобы избежать очистки управляемых ресурсов, так как мы не можем быть уверенными в их состоянии, что они до сих пор находятся в памяти. И в этом случае остается полагаться на деструкторы этих ресурсов. Ну и в обоих случаях освобождаются неуправляемые ресурсы.

    Еще один важный момент — вызов в методе Dispose метода GC.SuppressFinalize(this) . GC.SuppressFinalize не позволяет системе выполнить метод Finalize для данного объекта. Если же в классе деструктор не определен, то вызов этого метода не будет иметь никакого эффекта.

    Таким образом, даже если разработчик не использует в программе метод Dispose, все равно произойдет очистка и освобождение ресурсов.

    Общие рекомендации по использованию Finalize и Dispose
    • Деструктор следует реализовывать только у тех объектов, которым он действительно необходим, так как метод Finalize оказывает сильное влияние на производительность
    • После вызова метода Dispose необходимо блокировать у объекта вызов метода Finalize с помощью GC.SuppressFinalize
    • При создании производных классов от базовых, которые реализуют интерфейс IDisposable, следует также вызывать метод Dispose базового класса:
    public class Derived: Base < private bool IsDisposed = false; protected override void Dispose(bool disposing) < if (IsDisposed) return; if (disposing) < // Освобождение управляемых ресурсов >IsDisposed = true; // Обращение к методу Dispose базового класса base.Dispose(disposing); > >

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *