ようこそ。睡眠不足なプログラマのチラ裏です。

いまさら聞けない、IDisposableインターフェイス

マネジーリソースとアンマネージリソース

.NET Frameworkで扱うリソースの種類は、大きく2つに分けることができる。
CLRが管理するマネージリソース(GCヒープ)と、CLRが管理しないアンマネージリソース。


ガベコレによってオートマチックに解放されるのがマネージリソースで、
開発者が責任を持ってマニュアル管理しなければならないのがアンマネージリソース。
マイクロソフトから提供されているクラスについては、ガベコレがマネージリソースを回収するときに、
アンマネージリソースの解放も一緒に行われるが、それは、そのようにそのクラスが実装されているからにすぎない。
自分でアンマネージリソースを扱った実装をするクラスについては、
ガベコレはあくまでマネージリソースしか回収しないので、自らの手でアンマネージリソースを
解放してあげるように実装しなければ、いつまでたってもアンマネージリソースは解放されない。
また、ガベコレが回収を開始するのは、マネージリソースが足りなくなったときに限られる。
アンマネージリソースが足りなくなっても、ガベコレは何もしてくれない。
当然のことだが、アンマネージリソースの解放を怠るとメモリリークが発生する。


IDisposableインターフェイスを実装する目的

.NET Frameworkではガベコレに頼らず、コード上での明示の解放が必要であることを示すための
インターフェイスとしてSystem.IDisposableを提供している。
IDisposable インターフェイスが実装されているということは、
クラスがファイル、ストリーム、およびハンドルなどのアンマネージリソースを利用していることを、
クラスの利用者(開発者)に知らせることを目的として実装されていると考えると理解しやすい。


クラスがアンマネージリソースをフィールドに保持するような場合、
もしくはIDisposableインターフェイスを実装しているクラスを
フィールド上に保持するような場合などは、もちろんそのクラスもIDisposableインターフェイスを実装する必要があると言える。
間違ってもマネージリソースしか含まないクラスに対して無闇にIDisposableインターフェイスを実装したり、
必要なタイミングでDispose()が正しく呼び出されていなかったりすることがないようにしませう。


IDisposableを実装する際に満たすべき要件

まず、Dispose()では、マネージリソースとアンマネージリソースの
いずれのリソースについても解放処理を行う必要がある。
Dispose()が呼び出されなかった場合も考慮して、ガベコレによってマネージリソースの回収が行われる際に、
一緒にアンマネージリソースの解放もしてもらえるように、
デストラクタでは、アンマネージリソースの解放処理が行われるように実装する必要がある。


上記の条件を満たす場合、Dispose()が呼び出された後であれば、
既にマネージリソースもアンマネージリソースも解放済みとなっているため、デストラクタを呼び出す必要がない。
既に解放済みのリソースにもかかわらず、ガベコレの回収によって、デストラクタが呼び出されると
Finalizeキューが処理され、無駄にパフォーマンスを低下させることとなる。*1
したがって、このような場合は、GC.SuppressFinalize(obj)を用いて、
Finalizeの呼び出しが不要なオブジェクトを指定し、デストラクタがガベコレの対象外となるようにする。


また、Dispose()は複数回呼び出される可能性があるので、複数回呼び出されても問題のないように実装する必要がある。
Dispose()が呼び出された後にDispose()以外の公開メソッドを呼び出された場合、ObjectDisposedExceptionをthrowする。


以下、IDisposableインターフェイスの実装サンプル

    public class Releasable : IDisposable
    {
    
        #region メンバ

        /// <summary>
        /// Disposeされたかどうか
        /// </summary>
        private bool disposed;

        /// <summary>
        /// アンマネージリソースを管理しているクラスUnamnaged 
        /// </summary>
        private Unamnaged m_unmanaged;

        /// <summary>
        /// アンマネージリソースへのネイティブポインタ
        /// </summary>
        private IntPtr m_pUnmanaged;       
        
        #endregion

        #region コンストラクタ
        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public Releasable()
        {
            //Disposeされていませんよ
            this.disposed = false;
        }
        #endregion

        #region デストラクタ

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~Releasable()
        {
            //アンマネージリソースの解放
            this.Dispose(false);
        }
        
        #endregion

        /// <summary>
        /// Dispose
        /// シールクラスの場合ははprivateメソッドとする
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            lock (this){
                if (this.disposed)
                {
                    //既に呼びだしずみであるならばなんもしない
                    return;
                }

                this.disposed = true;

                if (disposing)
                {
                    // マネージリソースの解放
                    this.m_unmanaged.Dispose();
                    this.m_unmanaged = null;
                }

                // アンマネージリソースの解放
                if (this.m_pUnmanaged != IntPtr.Zero)
                {
                    PtrFree(this.m_pUnmanaged);
                    this.m_pUnmanaged = IntPtr.Zero;
                }
            }
        }

        /// <summary>
        /// なんらかの処理のメソッド
        /// </summary>
        public void SomethingMethod()
        {
            //すでにDispose済みの場合は例外throw
            this.ThrowExceptionIfDisposed();

            // SomethingMethodの処理
        }

        /// <summary>
        /// Dispose済みの場合ObjectDisposedExceptionをthrow
        /// クラスがシールクラスの場合ははprivateメソッドとする
        /// </summary>
        protected void ThrowExceptionIfDisposed()
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException(this.GetType().ToString());
            }
        }

        #region IDisposable メンバ

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            //マネージリソースおよびアンマネージリソースの解放
            this.Dispose(true);
            
            //ガベコレから、このオブジェクトのデストラクタを対象外とする
            GC.SuppressFinalize(this);
        }

        #endregion
    }

*1:デストラクタがいつ呼び出されるかはガベコレによって決定される。プログラマは制御することができない。ガベコレは、アプリケーションが使用していないオブジェクトをチェックします。参照されていないオブジェクトと考えられる場合、デストラクタ が存在する場合それを呼び出し、オブジェクトの格納に使用されているメモリを解放します。なお、デストラクタは、プログラムの終了時にも呼び出される。