Dispose 메서드 구현

Dispose 메서드는 주로 관리되지 않는 리소스를 해제하기 위해 구현됩니다. IDisposable 구현인 인스턴스 멤버를 사용하는 경우에는 Dispose 호출을 계단식 배열하는 것이 일반적입니다. 예를 들어 할당된 메모리를 해제하거나, 컬렉션에 추가된 항목을 제거하거나, 획득한 잠금 해제를 알리는 등 Dispose를 구현하는 다른 이유가 있습니다.

.NET 가비지 수집기는 관리되지 않는 메모리를 할당하거나 해제하지 않습니다. Dispose 패턴이라고도 하는 개체 삭제 패턴에서는 개체의 수명에 순서를 적용합니다. Dispose 패턴은 IDisposable 인터페이스를 구현하는 개체에 사용됩니다. 파일 및 파이프 핸들, 레지스트리 핸들, 대기 핸들 또는 관리되지 않는 메모리 블록에 대한 포인터와 상호 작용할 때 가비지 수집기가 관리되지 않는 개체를 회수할 수 없으므로 이 패턴이 일반적입니다.

리소스가 항상 적절하게 정리되게 하려면 Dispose 메서드가 멱등원(idempotent)이어야 합니다(예외를 throw하지 않고 여러 번 호출할 수 있음). 또한 Dispose의 후속 호출은 아무 작업도 수행하지 않아야 합니다.

GC.KeepAlive 메서드에 대해 제공된 코드 예에서는 개체 또는 개체의 멤버에 대한 관리되지 않는 참조가 여전히 사용 중인 동안 가비지 수집으로 인해 종료자가 실행되게 할 수 있는 방법을 보여 줍니다. GC.KeepAlive를 활용하여 현재 루틴의 시작부터 이 메서드가 호출되는 시점까지 개체를 가비지 수집에 부적절하도록 만드는 것이 좋을 수도 있습니다.

종속성 주입과 관련하여 IServiceCollection에 서비스를 등록하면 서비스 수명이 사용자를 대신하여 암시적으로 관리됩니다. IServiceProvider 및 해당 IHost는 리소스 정리를 오케스트레이션합니다. 특히, IDisposableIAsyncDisposable의 구현은 지정된 수명이 끝나면 적절하게 삭제됩니다.

자세한 내용은 .NET에서 종속성 주입을 참조하세요.

SafeHandle

개체 종료자에 대한 코드를 작성하는 작업은 올바르게 수행되지 않을 경우 문제를 일으킬 수 있는 복잡한 작업입니다. 따라서 종료자를 구현하는 대신 System.Runtime.InteropServices.SafeHandle 개체를 생성하는 것이 좋습니다.

System.Runtime.InteropServices.SafeHandle은 관리되지 않는 리소스를 식별하는 System.IntPtr을 래핑하는 관리되는 추상 형식입니다. Windows에서는 핸들을 식별하고 Unix에서는 파일 설명자를 식별할 수 있습니다. SafeHandleSafeHandle이 삭제되거나 SafeHandle에 대한 모든 참조가 삭제되고 SafeHandle 인스턴스가 종료될 때 한 번만 이 리소스가 해제되도록 하는 데 필요한 모든 논리를 제공합니다.

System.Runtime.InteropServices.SafeHandle은 추상 기본 클래스입니다. 파생 클래스는 다양한 종류의 핸들에 대해 특정 인스턴스를 제공합니다. 이러한 파생 클래스는 잘못된 것으로 간주되는 System.IntPtr 값과 실제로 핸들을 해제하는 방법의 유효성을 검사합니다. 예를 들어 SafeFileHandleSafeHandle에서 파생되어 열려 있는 파일 핸들/설명자를 식별하는 IntPtrs을 래핑하고 SafeHandle.ReleaseHandle() 메서드를 재정의하여 닫습니다(Unix의 close 함수 또는 Windows의 CloseHandle 함수를 통해). 관리되지 않는 리소스를 만드는 대부분의 .NET 라이브러리 API는 원시 포인터를 다시 전달하는 대신 SafeHandle에서 이를 래핑하고 필요에 따라 해당 SafeHandle을 사용자에게 반환합니다. 관리되지 않는 구성 요소와 상호 작용하고 관리되지 않는 리소스에 대한 IntPtr을 가져오는 경우 고유한 SafeHandle 형식을 만들어 래핑할 수 있습니다. 결과적으로 종료자를 구현해야 하는 SafeHandle 이외의 형식은 거의 없습니다. 대부분의 일회용 패턴 구현은 다른 관리되는 리소스만 래핑하게 되며 그중 일부는 SafeHandle 개체일 수 있습니다.

Microsoft.Win32.SafeHandles 네임스페이스에서 다음과 같은 파생된 클래스가 SafeHandle을 제공합니다.

클래스 보유하고 있는 리소스
SafeFileHandle
SafeMemoryMappedFileHandle
SafePipeHandle
파일, 메모리 매핑된 파일 및 파이프
SafeMemoryMappedViewHandle 메모리 뷰
SafeNCryptKeyHandle
SafeNCryptProviderHandle
SafeNCryptSecretHandle
암호화 구문
SafeRegistryHandle 레지스트리 키
SafeWaitHandle 대기 핸들

Dispose() 및 Dispose(bool)

IDisposable 인터페이스에서는 매개 변수가 없는 단일 메서드인 Dispose를 구현해야 합니다. 또한 봉인되지 않은 클래스에는 Dispose(bool) 오버로드 메서드가 있어야 합니다.

메서드 서명은 다음과 같습니다.

  • public 비가상(Visual Basic의 경우 NotOverridable)(IDisposable.Dispose 구현).
  • protected virtual(Visual Basic에서는 Overridable) Dispose(bool).

Dispose() 메서드

public, 비가상(Visual Basic의 경우 NotOverridable), 매개 변수 없는 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 매개 변수는 메서드 호출이 Dispose 메서드(값이 true)에서 수행되는지 또는 종료자(값이 false)에서 수행되는지를 나타내는 Boolean입니다.

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

Important

disposing 매개 변수가 종료자에서 호출되는 경우 false이고 IDisposable.Dispose 메서드에서 호출되는 경우 true여야 합니다. 즉, 결정적으로 호출되는 경우에는 true이고, 비결정적으로 호출되는 경우에는 false입니다.

메서드 본문은 세 가지 코드 블록으로 구성됩니다.

  • 개체가 이미 삭제된 경우 조건부 반환을 위한 블록입니다.

  • 관리되지 않는 리소스를 해제하는 블록. 이 블록은 disposing 매개 변수의 값에 관계없이 실행됩니다.

  • 관리되는 리소스를 해제하는 조건부 블록. 이 블록은 disposing 값이 true인 경우 실행됩니다. 해제되는 관리되는 리소스는 다음과 같습니다.

    • IDisposable을 구현하는 관리되는 개체. 조건부 블록을 사용하여 Dispose 구현(cascade dispose)을 호출할 수 있습니다. 관리되지 않는 리소스를 래핑하기 위해 System.Runtime.InteropServices.SafeHandle의 파생 클래스를 사용한 경우 여기에서 SafeHandle.Dispose() 구현을 호출해야 합니다.

    • 많은 메모리를 사용하거나 부족한 리소스를 사용하는 관리되는 개체. 관리되는 큰 개체 참조를 null에 할당하여 더 연결할 수 없는 상태로 만듭니다. 이렇게 하면 비결정적으로 회수된 경우보다 더 빠르게 해제됩니다.

메서드 호출이 종료자에서 수행된 경우 관리되지 않는 리소스를 해제하는 코드만 실행되어야 합니다. 구현자는 잘못된 경로가 삭제되었을 수 있는 관리 개체와 상호 작용하지 않도록 할 책임이 있습니다. 가비지 수집기가 마무리하는 동안 관리 개체를 삭제하는 순서가 비결정적이기 때문에 이는 중요합니다.

Cascade dispose 호출

클래스가 필드 또는 속성을 소유하고 해당 형식이 IDisposable을 구현하는 경우 포함하는 클래스 자체도 IDisposable을 구현해야 합니다. IDisposable 구현을 인스턴스화하고 이를 인스턴스 멤버로 저장하는 클래스도 해당 정리를 담당합니다. 이는 참조된 삭제 가능한 형식에 Dispose 메서드를 통해 결정적으로 정리를 수행할 수 있는 기회를 제공하는 데 도움이 됩니다. 다음 예에서 클래스는 sealed(또는 Visual Basic의 경우 NotInheritable)입니다.

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) 메서드 포함). 주된 이유 중 하나는 인스턴스가 완전히 초기화되었는지 확실하지 않은 경우입니다(예: 생성자에서 예외가 throw될 수 있음).

Dispose 패턴 구현

모든 봉인되지 않은 클래스 또는 NotInheritable로 수정되지 않는 Visual Basic 클래스는 상속될 수 있기 때문에 잠재적 기본 클래스로 간주해야 합니다. 잠재적 기본 클래스에 대한 삭제 패턴을 구현하는 경우 다음을 제공해야 합니다.

  • Dispose 메서드를 호출하는 Dispose(bool) 구현
  • 실제 정리를 수행하는 Dispose(bool) 메서드
  • 관리되지 않는 리소스를 래핑하는 SafeHandle에서 파생된 클래스(권장) 또는 Object.Finalize 메서드 재정의 SafeHandle 클래스는 종료자를 제공하므로 직접 작성할 필요가 없습니다.

Important

기본 클래스는 관리되는 개체만 참조할 수 있으며 Dispose 패턴을 구현할 수 있습니다. 이러한 경우 종료자는 필요하지 않습니다. 종료자는 관리되지 않는 리소스를 직접 참조하는 경우에만 필요합니다.

다음은 안전 핸들을 사용하는 기본 클래스에 대한 Dispose 패턴을 구현하는 일반적인 예입니다.

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()를 사용하여 종료자를 만듭니다.

파생 클래스에 대한 Dispose 패턴 구현

IDisposable의 기본 클래스 구현은 파생된 클래스에 의해 상속되므로 IDisposable 인터페이스를 구현하는 클래스에서 파생된 클래스가 IDisposable.Dispose을 구현하지 않아야 합니다. 대신 파생 클래스를 정리하려면 다음을 제공합니다.

  • 기본 클래스 메서드를 재정의하고 파생 클래스의 실제 정리를 수행하는 protected override void Dispose(bool) 메서드. 또한 이 메서드는 처리 상태(bool disposing 매개 변수)를 인수로 전달하여 base.Dispose(bool)(Visual Basic의 경우 MyBase.Dispose(bool)) 메서드를 호출해야 합니다.
  • 관리되지 않는 리소스를 래핑하는 SafeHandle에서 파생된 클래스(권장) 또는 Object.Finalize 메서드 재정의 SafeHandle 클래스는 사용자가 코딩할 필요가 없는 종료자를 제공합니다. 종료자를 제공하는 경우 false 인수를 사용하여 Dispose(bool) 오버로드를 호출해야 합니다.

다음은 안전 핸들을 사용하는 파생 클래스에 대한 Dispose 패턴을 구현하기 위한 일반 패턴의 예입니다.

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

참고 항목