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

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

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

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

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

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

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

System.Runtime.InteropServices.SafeHandle — это абстрактный управляемый тип, выполняющий роль оболочки для System.IntPtr, который идентифицирует неуправляемый ресурс. В среде Windows он может обозначать дескриптор, а в среде UNIX — дескриптор файла. Он обеспечивает всю логику, которая гарантирует, что при удалении 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:

Dispose() и Dispose(bool)

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

  • невиртуальный (public в Visual Basic) метод IDisposable.Dispose с атрибутом NonInheritable и без параметров;

  • метод Dispose с атрибутом protected virtual (Overridable в Visual Basic) со следующей сигнатурой:

    protected virtual void Dispose(bool disposing)
    {
    }
    
    Protected Overridable Sub Dispose(disposing As Boolean)
    End Sub
    

    Важно!

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

Метод Dispose()

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

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).

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

  • Блок, который освобождает неуправляемые ресурсы. Этот блок выполняется вне зависимости от значения параметра disposing.

  • Условный блок, который освобождает управляемые ресурсы. Этот блок выполняется, если параметр disposing имеет значение true. К управляемым ресурсам, которые он освобождает, могут относиться:

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

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

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

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

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

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

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

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

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

Важно!

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

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

Примечание

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

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

Совет

В 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) и передавать состояние освобождения ресурса в качестве аргумента.
  • Любой класс, производный от класса SafeHandle, который создает оболочку для неуправляемого ресурс (рекомендуется), или переопределенный метод Object.Finalize. Класс SafeHandle содержит метод завершения, что освобождает разработчика от необходимости создавать его вручную. Если есть метод завершения, он должен вызывать перегрузку Dispose(bool), указывая аргумент disposing со значением false.

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

Примечание

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

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

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

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

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

public class DisposableStreamResource : IDisposable
{
    // Define constants.
    protected const uint GENERIC_READ = 0x80000000;
    protected const uint FILE_SHARE_READ = 0x00000001;
    protected const uint OPEN_EXISTING = 3;
    protected const uint FILE_ATTRIBUTE_NORMAL = 0x80;
    private const int INVALID_FILE_SIZE = unchecked((int)0xFFFFFFFF);

    // Define Windows APIs.
    [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode)]
    protected static extern SafeFileHandle CreateFile(
        string lpFileName, uint dwDesiredAccess,
        uint dwShareMode, IntPtr lpSecurityAttributes,
        uint dwCreationDisposition, uint dwFlagsAndAttributes,
        IntPtr hTemplateFile);

    [DllImport("kernel32.dll")]
    private static extern int GetFileSize(
        SafeFileHandle hFile, out int lpFileSizeHigh);

    // Define locals.
    private bool _disposed = false;
    private readonly SafeFileHandle _safeHandle;
    private readonly int _upperWord;

    public DisposableStreamResource(string fileName)
    {
        if (string.IsNullOrWhiteSpace(fileName))
        {
            throw new ArgumentException("The fileName cannot be null or an empty string");
        }

        _safeHandle = CreateFile(
            fileName, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero,
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);

        // Get file size.
        Size = GetFileSize(_safeHandle, out _upperWord);
        if (Size == INVALID_FILE_SIZE)
        {
            Size = -1;
        }
        else if (_upperWord > 0)
        {
            Size = (((long)_upperWord) << 32) + Size;
        }
    }

    public long Size { get; }

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

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        // Dispose of managed resources here.
        if (disposing)
        {
            _safeHandle?.Dispose();
        }

        // Dispose of any unmanaged resources not wrapped in safe handles.

        _disposed = true;
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.IO

Public Class DisposableStreamResource : Implements IDisposable
    ' Define constants.
    Protected Const GENERIC_READ As UInteger = &H80000000UI
    Protected Const FILE_SHARE_READ As UInteger = &H0I
    Protected Const OPEN_EXISTING As UInteger = 3
    Protected Const FILE_ATTRIBUTE_NORMAL As UInteger = &H80
    Private Const INVALID_FILE_SIZE As Integer = &HFFFFFFFF

    ' Define Windows APIs.
    Protected Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (
        lpFileName As String, dwDesiredAccess As UInt32,
        dwShareMode As UInt32, lpSecurityAttributes As IntPtr,
        dwCreationDisposition As UInt32, dwFlagsAndAttributes As UInt32,
        hTemplateFile As IntPtr) As SafeFileHandle

    Private Declare Function GetFileSize Lib "kernel32" (
        hFile As SafeFileHandle, ByRef lpFileSizeHigh As Integer) As Integer

    ' Define locals.
    Private disposed As Boolean = False
    Private ReadOnly safeHandle As SafeFileHandle
    Private ReadOnly upperWord As Integer

    Public Sub New(fileName As String)
        If String.IsNullOrWhiteSpace(fileName) Then
            Throw New ArgumentNullException("The fileName cannot be null or an empty string")
        End If

        safeHandle = CreateFile(
            fileName, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero,
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero)

        ' Get file size.
        Size = GetFileSize(safeHandle, upperWord)
        If Size = INVALID_FILE_SIZE Then
            Size = -1
        ElseIf upperWord > 0 Then
            Size = (CLng(upperWord) << 32) + Size
        End If
    End Sub

    Public ReadOnly Property Size As Long

    Public Sub Dispose() _
              Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Protected Overridable Sub Dispose(disposing As Boolean)
        If disposed Then Exit Sub

        ' Dispose of managed resources here.
        If disposing Then
            safeHandle.Dispose()
        End If

        ' Dispose of any unmanaged resources not wrapped in safe handles.

        disposed = True
    End Sub
End Class

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

В следующем примере показан шаблон удаления для производного класса DisposableStreamResource2, унаследованного от класса DisposableStreamResource, представленного в предыдущем примере. Класс добавляет дополнительный метод WriteFileInfo и использует объект SafeFileHandle для создания экземпляра дескриптора записываемого файла.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;

public class DisposableStreamResource2 : DisposableStreamResource
{
    // Define additional constants.
    protected const uint GENERIC_WRITE = 0x40000000;
    protected const uint OPEN_ALWAYS = 4;

    // Define additional APIs.
    [DllImport("kernel32.dll")]
    protected static extern bool WriteFile(
        SafeFileHandle safeHandle, string lpBuffer,
        int nNumberOfBytesToWrite, out int lpNumberOfBytesWritten,
        IntPtr lpOverlapped);

    // To detect redundant calls
    private bool _disposed = false;
    private bool _created = false;
    private SafeFileHandle _safeHandle;
    private readonly string _fileName;

    public DisposableStreamResource2(string fileName) : base(fileName) => _fileName = fileName;

    public void WriteFileInfo()
    {
        if (!_created)
        {
            _safeHandle = CreateFile(
                @".\FileInfo.txt", GENERIC_WRITE, 0, IntPtr.Zero,
                OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);

            _created = true;
        }

        string output = $"{_fileName}: {Size:N0} bytes\n";
        _ = WriteFile(_safeHandle, output, output.Length, out _, IntPtr.Zero);
    }

    protected override void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return;
        }

        // Release any managed resources here.
        if (disposing)
        {
            // Dispose managed state (managed objects).
            _safeHandle?.Dispose();
        }

        // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
        // TODO: set large fields to null.

        _disposed = true;

        // Call the base class implementation.
        base.Dispose(disposing);
    }
}
Imports Microsoft.Win32.SafeHandles
Imports System.IO

Public Class DisposableStreamResource2 : Inherits DisposableStreamResource
    ' Define additional constants.
    Protected Const GENERIC_WRITE As Integer = &H40000000
    Protected Const OPEN_ALWAYS As Integer = 4

    ' Define additional APIs.
    Protected Declare Function WriteFile Lib "kernel32.dll" (
        safeHandle As SafeFileHandle, lpBuffer As String,
        nNumberOfBytesToWrite As Integer, ByRef lpNumberOfBytesWritten As Integer,
        lpOverlapped As Object) As Boolean

    ' Define locals.
    Private disposed As Boolean = False
    Private created As Boolean = False
    Private safeHandle As SafeFileHandle
    Private ReadOnly filename As String

    Public Sub New(filename As String)
        MyBase.New(filename)
        Me.filename = filename
    End Sub

    Public Sub WriteFileInfo()
        If Not created Then
            safeHandle = CreateFile(
                ".\FileInfo.txt", GENERIC_WRITE, 0, IntPtr.Zero,
                OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero)
            created = True
        End If

        Dim output As String = $"{filename }: {Size:N0} bytes {vbCrLf }"
        Dim result = WriteFile(safeHandle, output, output.Length, 0&, Nothing)
    End Sub

    Protected Overridable Overloads Sub Dispose(disposing As Boolean)
        If disposed Then Exit Sub

        ' Release any managed resources here.
        If disposing Then
            safeHandle?.Dispose()
        End If

        disposed = True
        ' Release any unmanaged resources not wrapped by safe handles here.

        ' Call the base class implementation.
        MyBase.Dispose(disposing)
    End Sub
End Class

См. также