C#/C# 인터페이스

IDisposable : 비관리 리소스 해제하기

로파이 2021. 11. 12. 23:51

IDisposable 인터페이스

 

비관리 리소스를 해제하는 방법을 제공한다.

public interface IDisposable

 

C#에서는 GC가 참조 형식의 객체의 메모리를 자동으로 관리하며, 더 이상 참조되지 않아 메모리가 해제될 때 비관리 리소스를 갖고있는 객체가 해당 인터페이스를 통해 직접 해제할 수 있는 메커니즘을 제공한다.

어느 시점에 해당 리소스가 해제될 지 (GC가 언제 릴리즈할지) 정확한 예측을 할 수는 없으며, GC는 관리되지 않는 리소스(윈도우 핸들이나, 파일 핸들, 스트림 등)를 알 길이 없다.

 

GC와 더불어 사용자가 직접 판단하에 해당 객체가 더 이상 필요없다고 판단할 경우 직접 Dispose()를 호출하여 비관리 리소스를 해제할 수 있다.

 

IDisposable.Dispose의 구현은 인스턴스가 더 이상 필요하지 않을 때 호출되므로 SafeHandle과 같은 관리 객체를 래핑해서 사용하거나 Finalizer를 구현해서 직접 비관리 리소스를 해제하도록 유도해야한다.

    class DisposeExample 
    {
        public class MyResource : IDisposable
        {
            private IntPtr Handle; // 외부 비관리 리소스 핸들
            private Component component = new Component(); // 관리 리소스
            private bool disposed = false; // Dispose가 호출되었는지 확인한다.

            public MyResource(IntPtr _Handle)
            {
                Handle = _Handle;
            }

            public void Dispose()
            {
                Dispose(disposing: true);

                // 사용자가 직접 Dispose()를 호출했기 때문에 
                // GC는 finalizer를 호출할 필요가 없다.
                // GC는 finalizer를 구현한 인스턴스에 대해 finalizer를 호출한다.
                GC.SuppressFinalize(this);
            }

            // disposing이 true인 경우 사용자가 직접 호출하여
            // 해당 인스턴스의 관리/비관리 리소스를 해제하고 있는 것이다.
            // disposing이 false인 경우, finalizer에 의해 호출되어
            // 다른 객체를 더 이상 참조하면 안되기 때문에 오직 비관리 리소스만 해제되어야한다.
            protected virtual void Dispose(bool disposing)
            {
                // 이미 리소스가 해제 되었는지 확인
                if (disposed == true)
                {
                    return;
                }

                // 실제로 사용자가 호출한 경우에만 
                // 관리 리소스를 호출한다.
                if(disposing)
                {
                    component.Dispose();
                }

                // 비관리 리소스를 해제하기 위해
                // 올바른 함수를 호출한다.
                CloseHandle(Handle);
                Handle = IntPtr.Zero;

                // 이제 해당 인스턴스는 모두 해제되었다.
                disposed = true;
            }

            // 비관리 리소스를 해제하는 닷넷의 Interop 인터페이스를 사용한다.
            [System.Runtime.InteropServices.DllImport("Kernel32")]
            private extern static Boolean CloseHandle(IntPtr handle);

            // C# finalizer는 비관리 리소스만 해제해야한다.
            ~MyResource() => Dispose(disposing: false);
        }
    }

Dispose 인터페이스의 구현

1. Dispose(bool disposing) 사용자가 직접 호출하고 있는 지 혹은 finalizer에 의해 호출되고 있는지 알기 위해 disposing을 전달한다.

2. Dispose() 내부에서는 사용자가 직접 호출한 경우(disposing : true) 관리/비관리 리소스를 모두 해제시킬 수 있다.

3. Finalizer에 의해 호출된 경우(disposing : false) 참조를 만들면 문제가 생길 수 있으므로 비관리 리소스만 해제하도록 한다.

4. disposed 부울 변수는 해당 Dispose() 함수가 호출된 적이 있는지 확인한다. 한번 호출된 적이 있다면 두 번이상 호출할 필요가 없다.

 

 

using 키워드

using 키워드는 비관리 리소스를 해제해야하는 인스턴스가 예외 안전하게 항상 비관리 리소스를 해제하도록 try-finally를 컴파일러가 IL 언어로 만들어주도록 한다.

using System;
using System.IO;
using System.Text.RegularExpressions;

public class WordCount
{
   private String filename = String.Empty;
   private int nWords = 0;
   private String pattern = @"\b\w+\b";

   public WordCount(string filename)
   {
      if (! File.Exists(filename))
         throw new FileNotFoundException("The file does not exist.");

      this.filename = filename;
      string txt = String.Empty;
      using (StreamReader sr = new StreamReader(filename)) {
         txt = sr.ReadToEnd();
      }
      nWords = Regex.Matches(txt, pattern).Count;
      /*
      StreamReader sr = null;
      try {
         sr = new StreamReader(filename);
         txt = sr.ReadToEnd();
      }
      finally {
         if (sr != null) sr.Dispose();
      }
      nWords = Regex.Matches(txt, pattern).Count;
      */
   }

   public string FullName
   { get { return filename; } }

   public string Name
   { get { return Path.GetFileName(filename); } }

   public int Count
   { get { return nWords; } }
}

 

Base-Derived 상속 관계에서 올바르게 Dispose를 구현하기

Base 클래스에서는 상속을 생각하지 않은 경우와 마찬가지로 인터페이스를 구현해주면 된다.

- Dispose()에서 Dispose(disposing : true)를 호출한다. GC의 Finalizer 호출을 막는다.

- Dispose(bool disposing) : 관리/비관리 리소스를 해제한다.

- Finalizer : Dispose(false)를 호출한다.

using System;

class BaseClass : IDisposable
{
   // Flag: Has Dispose already been called?
   bool disposed = false;

   // Public implementation of Dispose pattern callable by consumers.
   public void Dispose()
   {
      Dispose(disposing: true);
      GC.SuppressFinalize(this);
   }

   // Protected implementation of Dispose pattern.
   protected virtual void Dispose(bool disposing)
   {
      if (disposed)
         return;

      if (disposing) {
         // Free any other managed objects here.
         //
      }

      // Free any unmanaged objects here.
      //
      disposed = true;
   }

   ~BaseClass()
   {
      Dispose(disposing: false);
   }
}

Derived 파생 클래스에서는 Finalizer를 정의하지 않는다. 대신 Dispose(bool disposing)을 상속해서 관리/비관리 리소스를 해제한 뒤에 base.Dispose(disposing)을 호출하도록한다.

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

class DerivedClass : BaseClass
{
   // Flag: Has Dispose already been called?
   bool disposed = false;
   // Instantiate a SafeHandle instance.
   SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);

   // Protected implementation of Dispose pattern.
   protected override void Dispose(bool disposing)
   {
      if (disposed)
         return;

      if (disposing) {
         handle.Dispose();
         // Free any other managed objects here.
         //
      }

      // Free any unmanaged objects here.
      //

      disposed = true;
      // Call base class implementation.
      base.Dispose(disposing);
   }
}

 

참고 : https://docs.microsoft.com/en-us/dotnet/api/system.idisposable?view=net-5.0