Реализуйте метод Dispose
Метод Dispose в основном реализуется для выпуска неуправляемых ресурсов. При работе с элементами экземпляра, которые являются реализацией IDisposable, обычно происходит каскадирование вызовов Dispose. Существуют и другие причины реализации Dispose, например для освобождения выделенной памяти, удаления элемента, добавленного в коллекцию, или сигнала о снятии блокировки, полученной.
сборщик мусора .NET не выделяет или не освобождает неуправляемую память. Шаблон удаления объекта, называемый шаблон удаления, накладывает порядок на время существования объекта. Шаблон удаления используется для объектов, реализующих интерфейс IDisposable. Этот шаблон распространен при взаимодействии с дескрипторами файлов и каналов, дескрипторами реестра, дескрипторами ожидания или указателями на блоки неуправляемой памяти, так как сборщик мусора не может освободить неуправляемые объекты.
Чтобы убедиться, что ресурсы всегда очищаются соответствующим образом, метод Dispose должен быть идемпотентным, так, чтобы его можно было вызывать несколько раз без возникновения исключений. Кроме того, последующие вызовы Dispose не должны ничего делать.
В примере кода, предоставленном для метода GC.KeepAlive, показано, как сборка мусора может привести к запуску финализатора, в то время как неуправляемая ссылка на объект или его члены все еще используется. Может потребоваться использовать GC.KeepAlive, чтобы сделать объект недопустимым для сборки мусора с начала текущей подпрограммы до точки вызова этого метода.
Совет
Что касается внедрения зависимостей, при регистрации служб в IServiceCollectionвремя существования службы управляется неявно в ваших интересах. IServiceProvider и соответствующий IHost координируют процесс очистки ресурсов. В частности, реализации IDisposable и IAsyncDisposable подлежат корректному удалению в конце указанного срока службы.
Дополнительные сведения см. в статье о внедрении зависимостей в .NET.
Каскадные вызовы удаления
Если класс владеет экземпляром другого типа, реализующего 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. Как правило, класс, создающий и сохраняющий дочерний объект IDisposable, также становится владельцем, но в некоторых случаях владение может быть передано другому типу IDisposable.
- Существуют случаи, когда может потребоваться выполнить
null
-проверку в финализаторе (который включает методDispose(false)
, вызываемый финализатором). Одна из основных причин заключается в том, что вы не уверены, был ли экземпляр полностью инициализирован (например, исключение могло быть сгенерировано в конструкторе).
Dispose() и Dispose(bool)
Интерфейс IDisposable требует реализации одного метода без параметров Dispose. Кроме того, любой незапечатанный класс должен иметь перегружаемый метод Dispose(bool)
.
Подписи методов:
-
public
не виртуальные (NotOverridable
в Visual Basic) (реализацияIDisposable.Dispose). -
protected virtual
(Overridable
в Visual BasicDispose(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(bool)
В перегруженной версии параметр disposing
представляет собой Boolean, указывающий, происходит ли вызов метода из метода Dispose (его значение true
) или из финализатора (его значение false
).
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// Dispose managed state (managed objects).
// ...
}
// Free unmanaged resources.
// ...
_disposed = true;
}
Protected Overridable Sub Dispose(disposing As Boolean)
If disposed Then Exit Sub
If disposing Then
' Free managed resources.
' ...
End If
' Free unmanaged resources.
' ...
disposed = True
End Sub
Важное
Параметр disposing
должен быть false
при вызове из финализатора, а true
при вызове из метода IDisposable.Dispose. Другими словами, это true
при детерминированном вызове и false
при недетерминированном вызове.
Текст метода состоит из трех блоков кода:
Блок условного возврата, если объект уже удален.
Условный блок, который освобождает управляемые ресурсы. Этот блок выполняется, если значение
disposing
равноtrue
. Управляемые ресурсы, которые он освобождает, могут включать:- Управляемые объекты, реализующие IDisposable. Условный блок можно использовать для вызова реализации Dispose (каскадного удаления). Если вы использовали производный класс System.Runtime.InteropServices.SafeHandle для упаковки неуправляемого ресурса, необходимо вызвать реализацию SafeHandle.Dispose() здесь.
- Управляемые объекты, использующие большие объемы памяти или использующие ограниченные ресурсы. Присвойте ссылки на крупные управляемые объекты
null
, чтобы сделать их более труднодоступными. Это освобождает их быстрее, чем если бы они были восстановлены недетерминированно.
Блок, который освобождает неуправляемые ресурсы. Этот блок выполняется независимо от значения параметра
disposing
.
Если вызов метода поступает из средства завершения, должен выполняться только код, который освобождает неуправляемые ресурсы. Реализатор несет ответственность за то, чтобы ложный путь не взаимодействовал с управляемыми объектами, которые могли быть очищены. Это важно, так как порядок удаления управляемых объектов сборщиком мусора во время завершения недетерминирован.
Реализация шаблона удаления
Все неконечные классы (или классы Visual Basic, не изменённые как NotInheritable
) должны считаться потенциальными базовыми классами, так как они могут быть унаследованы. Если вы реализуете шаблон удаления для любого потенциального базового класса, необходимо добавить в класс следующие методы:
- Реализация Dispose, которая вызывает метод
Dispose(bool)
. - Метод
Dispose(bool)
, выполняющий фактическую очистку. - Если ваш класс сталкивается с неуправляемыми ресурсами, предоставьте переопределение метода Object.Finalize или оберните неуправляемый ресурс в SafeHandle.
Важное
Финализатор (Object.Finalize переопределение) требуется только в том случае, если вы напрямую ссылаетесь на неуправляемые ресурсы. Это очень сложный сценарий, который обычно можно избежать:
- Если класс ссылается только на управляемые объекты, класс по-прежнему может реализовать шаблон удаления. Нет необходимости реализовывать финализатор.
- Если вам нужно иметь дело с неуправляемыми ресурсами, настоятельно рекомендуется упаковать неуправляемый IntPtr дескриптор в SafeHandle. SafeHandle предоставляет финализатор, поэтому вам не нужно писать его самостоятельно. Дополнительные сведения см. в абзаце раздела Безопасные дескрипторы.
Базовый класс с управляемыми ресурсами
Ниже приведен общий пример реализации шаблона удаления для базового класса, которому принадлежат только управляемые ресурсы.
using System;
using System.IO;
public class DisposableBase : IDisposable
{
// Detect redundant Dispose() calls.
private bool _isDisposed;
// Instantiate a disposable object owned by this class.
private Stream? _managedResource = new MemoryStream();
// 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 (!_isDisposed)
{
_isDisposed = true;
if (disposing)
{
// Dispose managed state.
_managedResource?.Dispose();
_managedResource = null;
}
}
}
}
Imports System.IO
Public Class DisposableBase
Implements IDisposable
' Detect redundant Dispose() calls.
Private _isDisposed As Boolean
' Instantiate a disposable object owned by this class.
Private _managedResource As Stream = New MemoryStream()
' 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(disposing As Boolean)
If Not _isDisposed Then
_isDisposed = True
If disposing Then
' Dispose managed state.
_managedResource?.Dispose()
_managedResource = Nothing
End If
End If
End Sub
End Class
Заметка
В предыдущем примере для иллюстрации шаблона используется фиктивный объектMemoryStream. Вместо этого можно использовать любую маркировку IDisposable.
Базовый класс с неуправляемыми ресурсами
Вот пример реализации шаблона освобождения для базового класса, который переопределяет Object.Finalize в целях очистки неуправляемых ресурсов, которыми он владеет. В этом примере также демонстрируется способ реализации Dispose(bool)
в потокобезопасном режиме. Синхронизация может быть критической при работе с неуправляемыми ресурсами в многопотоковом приложении. Как упоминалось ранее, это расширенный сценарий.
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
public class DisposableBaseWithFinalizer : IDisposable
{
// Detect redundant Dispose() calls in a thread-safe manner.
// _isDisposed == 0 means Dispose(bool) has not been called yet.
// _isDisposed == 1 means Dispose(bool) has been already called.
private int _isDisposed;
// Instantiate a disposable object owned by this class.
private Stream? _managedResource = new MemoryStream();
// A pointer to 10 bytes allocated on the unmanaged heap.
private IntPtr _unmanagedResource = Marshal.AllocHGlobal(10);
~DisposableBaseWithFinalizer() => 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)
{
// In case _isDisposed is 0, atomically set it to 1.
// Enter the branch only if the original value is 0.
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
if (disposing)
{
_managedResource?.Dispose();
_managedResource = null;
}
Marshal.FreeHGlobal(_unmanagedResource);
}
}
}
Imports System
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Threading
Public Class DisposableBaseWithFinalizer
Implements IDisposable
' Detect redundant Dispose() calls in a thread-safe manner.
' _isDisposed == 0 means Dispose(bool) has not been called yet.
' _isDisposed == 1 means Dispose(bool) has been already called.
Private _isDisposed As Integer
' Instantiate a disposable object owned by this class.
Private _managedResource As Stream = New MemoryStream()
' A pointer to 10 bytes allocated on the unmanaged heap.
Private _unmanagedResource As IntPtr = Marshal.AllocHGlobal(10)
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(disposing As Boolean)
' In case _isDisposed is 0, atomically set it to 1.
' Enter the branch only if the original value is 0.
If Interlocked.CompareExchange(_isDisposed, 1, 0) = 0 Then
If disposing Then
_managedResource?.Dispose()
_managedResource = Nothing
End If
Marshal.FreeHGlobal(_unmanagedResource)
End If
End Sub
End Class
Заметка
- В предыдущем примере используется AllocHGlobal для выделения 10 байтов в неуправляемой куче в конструкторе и освобождения буфера в
Dispose(bool)
путем вызова FreeHGlobal. Это фиктивное выделение для иллюстрации. - Опять же, рекомендуется не использовать финализатор. См. раздел «Реализация шаблона удаления с помощью пользовательского безопасного дескриптора» для эквивалента предыдущего примера, который делегирует недетерминированное завершение и синхронизацию SafeHandle.
Совет
В 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 System.IO;
public class DisposableDerived : DisposableBase
{
// To detect redundant calls
private bool _isDisposed;
// Instantiate a disposable object owned by this class.
private Stream? _managedResource = new MemoryStream();
// Protected implementation of Dispose pattern.
protected override void Dispose(bool disposing)
{
if (!_isDisposed)
{
_isDisposed = true;
if (disposing)
{
_managedResource?.Dispose();
_managedResource = null;
}
}
// Call base class implementation.
base.Dispose(disposing);
}
}
Imports System.IO
Public Class DisposableDerived
Inherits DisposableBase
' To detect redundant calls
Private _isDisposed As Boolean
' Instantiate a disposable object owned by this class.
Private _managedResource As Stream = New MemoryStream()
' Protected implementation of Dispose pattern.
Protected Overrides Sub Dispose(disposing As Boolean)
If Not _isDisposed Then
_isDisposed = True
If disposing Then
_managedResource?.Dispose()
_managedResource = Nothing
End If
End If
' Call base class implementation.
MyBase.Dispose(disposing)
End Sub
End Class
Заметка
В предыдущем примере для иллюстрации шаблона используется объект SafeFileHandle; вместо этого можно использовать любой объект, производный от SafeHandle. Обратите внимание, что в примере не выполняется корректное создание экземпляра объекта SafeFileHandle.
Ниже приведен общий шаблон реализации шаблона удаления для производного класса, который переопределяет Object.Finalize:
using System.Threading;
public class DisposableDerivedWithFinalizer : DisposableBaseWithFinalizer
{
// Detect redundant Dispose() calls in a thread-safe manner.
// _isDisposed == 0 means Dispose(bool) has not been called yet.
// _isDisposed == 1 means Dispose(bool) has been already called.
private int _isDisposed;
~DisposableDerivedWithFinalizer() => Dispose(false);
// Protected implementation of Dispose pattern.
protected override void Dispose(bool disposing)
{
// In case _isDisposed is 0, atomically set it to 1.
// Enter the branch only if the original value is 0.
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
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.
}
// Call the base class implementation.
base.Dispose(disposing);
}
}
Imports System.Threading
Public Class DisposableDerivedWithFinalizer
Inherits DisposableBaseWithFinalizer
' Detect redundant Dispose() calls in a thread-safe manner.
' _isDisposed == 0 means Dispose(bool) has not been called yet.
' _isDisposed == 1 means Dispose(bool) has been already called.
Private _isDisposed As Integer
Protected Overrides Sub Finalize()
Dispose(False)
End Sub
' Protected implementation of Dispose pattern.
Protected Overrides Sub Dispose(disposing As Boolean)
' In case _isDisposed is 0, atomically set it to 1.
' Enter the branch only if the original value is 0.
If Interlocked.CompareExchange(_isDisposed, 1, 0) = 0 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.
End If
' Call the base class implementation.
MyBase.Dispose(disposing)
End Sub
End Class
Безопасные хендлы
Написание кода для средства завершения объекта — это сложная задача, которая может вызвать проблемы, если это не сделано правильно. Поэтому рекомендуется создавать объекты 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
объектами.
Реализация шаблона удаления с помощью пользовательского безопасного дескриптора
В следующем коде показано, как обрабатывать неуправляемые ресурсы путем реализации SafeHandle.
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
// Wraps the IntPtr allocated by Marshal.AllocHGlobal() into a SafeHandle.
class LocalAllocHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private LocalAllocHandle() : base(ownsHandle: true) { }
// No need to implement a finalizer - SafeHandle's finalizer will call ReleaseHandle for you.
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
// Allocate bytes with Marshal.AllocHGlobal() and wrap the result into a SafeHandle.
public static LocalAllocHandle Allocate(int numberOfBytes)
{
IntPtr nativeHandle = Marshal.AllocHGlobal(numberOfBytes);
LocalAllocHandle safeHandle = new LocalAllocHandle();
safeHandle.SetHandle(nativeHandle);
return safeHandle;
}
}
public class DisposableBaseWithSafeHandle : IDisposable
{
// Detect redundant Dispose() calls.
private bool _isDisposed;
// Managed disposable objects owned by this class
private LocalAllocHandle? _safeHandle = LocalAllocHandle.Allocate(10);
private Stream? _otherUnmanagedResource = new MemoryStream();
// 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 (!_isDisposed)
{
_isDisposed = true;
if (disposing)
{
// Dispose managed state.
_otherUnmanagedResource?.Dispose();
_safeHandle?.Dispose();
_otherUnmanagedResource = null;
_safeHandle = null;
}
}
}
}
Imports System
Imports System.IO
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles
' Wraps the IntPtr allocated by Marshal.AllocHGlobal() into a SafeHandle.
Public Class LocalAllocHandle
Inherits SafeHandleZeroOrMinusOneIsInvalid
Private Sub New()
MyBase.New(True)
End Sub
' No need to implement a finalizer - SafeHandle's finalizer will call ReleaseHandle for you.
Protected Overrides Function ReleaseHandle() As Boolean
Marshal.FreeHGlobal(handle)
Return True
End Function
' Allocate bytes with Marshal.AllocHGlobal() and wrap the result into a SafeHandle.
Public Shared Function Allocate(numberOfBytes As Integer) As LocalAllocHandle
Dim nativeHandle As IntPtr = Marshal.AllocHGlobal(numberOfBytes)
Dim safeHandle As New LocalAllocHandle()
safeHandle.SetHandle(nativeHandle)
Return safeHandle
End Function
End Class
Public Class DisposableBaseWithSafeHandle
Implements IDisposable
' Detect redundant Dispose() calls.
Private _isDisposed As Boolean
' Managed disposable objects owned by this class
Private _safeHandle As LocalAllocHandle = LocalAllocHandle.Allocate(10)
Private _otherUnmanagedResource As Stream = New MemoryStream()
' 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(disposing As Boolean)
If Not _isDisposed Then
_isDisposed = True
If disposing Then
' Dispose managed state.
_otherUnmanagedResource?.Dispose()
_safeHandle?.Dispose()
_otherUnmanagedResource = Nothing
_safeHandle = Nothing
End If
End If
End Sub
End Class
Заметка
Поведение класса DisposableBaseWithSafeHandle
эквивалентно поведению класса DisposableBaseWithFinalizer
в предыдущем примере, однако приведенный здесь подход является более безопасным:
- Нет необходимости реализовать финализатор, так как SafeHandle будет заботиться о завершении.
- Для обеспечения безопасности потоков не требуется синхронизация. Несмотря на то что в реализации
Dispose
есть состояние гонки вDisposableBaseWithSafeHandle
, SafeHandle гарантирует, что SafeHandle.ReleaseHandle будет вызван только один раз.
Встроенные безопасные дескрипторы SafeHandle в .NET
Следующие производные классы в пространстве имен Microsoft.Win32.SafeHandles предоставляют безопасные дескрипторы.
Класс | Ресурсы, которые он содержит |
---|---|
SafeFileHandle SafeMemoryMappedFileHandle SafePipeHandle |
Файлы, файлы, отображенные в памяти, и каналы |
SafeMemoryMappedViewHandle | Виды памяти |
SafeNCryptKeyHandle SafeNCryptProviderHandle SafeNCryptSecretHandle |
Конструкции криптографии |
SafeRegistryHandle | Ключи реестра |
SafeWaitHandle | Дескриптор ожидания |