LoginSignup
144
126

More than 1 year has passed since last update.

C# の Dispose を正しく実装する

Last updated at Posted at 2020-02-07

C# の Dispose を正しく実装する

IDisposable インターフェースの実装に焦点を絞った記事です。
using 構文による自動解放や、Finalizeや、GCのメカニズムについては、本記事末尾の資料をはじめとして、ネット上に良記事が沢山あるのでそちらを参考にしてください。

IDisposable インターフェース

https://referencesource.microsoft.com/#mscorlib/system/idisposable.cs,1f55292c3174123d より抜粋

namespace System {
[System.Runtime.InteropServices.ComVisible(true)]
    public interface IDisposable {
        void Dispose();
    }
}

とてもシンプルです。
IDisposable が求めているのは、void Dispose() を実装することのみです。
disposeパターンで述べられている void Dispose(bool disposing) は実装を要求されていません。

Dispose() のやるべき処理

void Dispose() は次の条件を満たす必要があります

https://referencesource.microsoft.com/#mscorlib/system/idisposable.cs より抜粋

    // Dispose should meet the following conditions:
    // 1) Be safely callable multiple times
    // 2) Release any resources associated with the instance
    // 3) Call the base class's Dispose method, if necessary
    // 4) Suppress finalization of this class to help the GC by reducing the
    //    number of objects on the finalization queue.
    // 5) Dispose shouldn't generally throw exceptions, except for very serious 
    //    errors that are particularly unexpected. (ie, OutOfMemoryException)  
    //    Ideally, nothing should go wrong with your object by calling Dispose.
抄訳
  1. 安全に複数回呼び出し可能であること
  2. インスタンスが保持しているリソースを解放すること
  3. 必要ならば、基本クラスのDisposeメソッドを呼び出すこと
  4. このクラスのファイナライズを抑制して、GCがファイナライズキュー上のオブジェクト数を減らすための手助けをする
  5. 予期せぬ非常に重大なエラー(たとえばOutOfMemoryException)を除いて、Disposeは例外をスローすべきではない。理想的には、Disposeを呼び出しにおいてオブジェクトに問題が発生しないようにする

ここで言うリソースとは、ファイルストリームやデータベースコネクションなどの、OSや外部DLLが管理するハンドルに紐づいたものです。
そして、C#のCLRの管理下にあるものをマネージドリソース、管理外にあるものをアンマネージドリソースと呼びます。
この両者の区別については細かい議論がありますが、IDisposable の実装に際しての分類は次の通りです。

IDisposable の実装におけるマネージドリソースとは

IDisposable インタフェースを実装したクラスのインスタンスです。

IDisposable の実装におけるアンマネージドリソースとは

IntPtrなどCLR外部(OSやDLLなど)のAPIから得たハンドル値です。

それらのハンドル値は、SafeHandle のサブクラスでラップすることで、マネージドリソースに変換できます。
処理中の例外発生に対して安全になるので、特別な理由がない限りマネージドリソースに変換したほうが良いです。

アンマネージドリソースを持つ場合の実装

disposeパターンに従って、マネージドリソースとアンマネージドリソースを解放します。

  • ベースクラスはIDisposableを継承し、Dispose() と Dispose(bool disposing) と ファイナライザ(デストラクタ)を実装します。
  • 解放処理は Dispose(bool disposing) に実装し、Dispose() から Dispose(disposing:true) で呼び出し、ファイナライザ(デストラクタ) からも Dispose(disposing:false) で呼び出します。
  • サブクラス側は、Dispose(bool disposing) のみをオーバライドします。
  • Dispose(disposing:false)で呼ばれた場合は、マネージドリソースの解放処理は行わずにGCにその解放処理を任せます。

なぜ、このような面倒な仕組みが必要なのでしょうか。
それは、プログラムがDispose()を呼ぶ前にGCがインスタンスを廃棄すると、インスタンスが持っているアンマネージドリソースが永遠に解放されない(リソースリーク)問題を避けるためです。
Dispose() と ファイナライザ(デストラクタ) の二か所でアンマネージドリソースの解放処理を行うことでも回避できますが、二か所に同じ処理を書くのは保守性が悪いので、一か所でまとめて処理するための仕組みなのです。
※ こういう仕組み(disposeパターン)を必要とするのは、C#言語がリソース管理の初期設計に失敗したからだと思います。

//  IDisposableを実装するベースクラスのdisposeパターン
public class MyBaseClass : IDisposable
{
    // Pointer to an external unmanaged resource.
    private IntPtr _handle;
    // Other managed resource this class uses.
    private Stream _stream;
    // Track whether Dispose has been called.
    private bool _disposed = false;

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

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: Dispose managed resources here.
                _stream.Dispose();
            }

            // TODO: Free unmanaged resources here.
            MyCloseHandle(_handle);
            _handle = IntPtr.Zero;

            // Note disposing has been done.
            _disposed = true;
        }
    }

    ~MyBaseClass()
    {
        Dispose(false);
    }

    protected static void MyCloseHandle(IntPtr handle)
    {
        // TODO: free the unmanaged "handle".
    }
}

// 上記クラスを先祖に持つサブクラスのdisposeパターン.
// public void Dispose() を上書きせず、void Dispose(bool disposing) を上書きする.
public class MySubClass : MyBaseClass
{
    // Pointer to an external unmanaged resource.
    private IntPtr _handle2;
    // Other managed resource this class uses.
    private Stream _stream2;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    protected override void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: Dispose managed resources here.
                _stream2.Dispose();
            }

            // TODO: Free unmanaged resources here.
            MyCloseHandle(_handle2);
            _handle2 = IntPtr.Zero;

            // Note disposing has been done.
            _disposed = true;

            // Call the base class implementation.
            base.Dispose(disposing);
        }
    }

    // ファイナライザでやるべきことは ~MyBaseClass() で済んでいるので、ファイナライザ ~MySubClass() は不要である.
    //~MySubClass()
    //{
    //    Dispose(false);
    //}
}

マネージドリソースのみを持つ場合の実装(disposeパターン)

disposeパターンに従って、マネージドリソースを解放します。

  • ベースクラスはIDisposableを継承し、Dispose() と Dispose(bool disposing) を実装します。
  • サブクラス側は、Dispose(bool disposing) のみをオーバライドします。
  • Dispose(disposing:false)で呼ばれた場合は、マネージドリソースの解放処理は行わずにGCにその解放処理を任せます。
// ベースクラスのdisposeパターン.
public class MyBaseClass : IDisposable
{
    // managed resource this class uses.
    private Stream _stream;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        // GC.SuppressFinalize(this);
        // ファイナライザを持たないthisに対して SuppressFinalize(this) は no effect なので、呼び出しは無意味である.
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: Dispose managed resources here.
                _stream.Dispose();
            }

            // Note disposing has been done.
            _disposed = true;
        }
    }

    // アンマネージドリソースを持たないので、ファイナライザ ~MyBaseClass() は不要である.
}

// サブクラスのdisposeパターン.
// public void Dispose() を上書きせず、void Dispose(bool disposing) をオーバライドする.
public class MySubClass : MyBaseClass
{
    // managed resource this class uses.
    private Stream _stream2;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    protected override void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: Dispose managed resources here.
                _stream2.Dispose();
            }

            // Note disposing has been done.
            _disposed = true;

            // Call the base class implementation.
            base.Dispose(disposing);
        }
    }

    // アンマネージドリソースを持たないので、ファイナライザ ~MySubClass() は不要である.
}

bool _disposed フラグを省略するコーディングも可能です。
処理対象のマネージドリソースが少ない場合は、このコーディングの方が簡潔で良いでしょう。

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // TODO: Dispose managed resources here.
            _stream?.Dispose();
            _stream = null;
        }
        // Call the base class implementation.
        base.Dispose(disposing);
    }

複数回呼び出しの際に base.Dispose(disposing); の呼び出しが冗長になるのが欠点ですが、ベースクラス側も安全に複数回呼び出し可能であれば動作上の問題はありません。

マネージドリソースのみを持つ場合の実装(disposeパターンを使わない簡易版)

アンマネージドリソースを持たない場合は、もっと単純に Dispose を実装するだけでOKです。

ただしサブクラスもふくめて絶対にアンマネージドリソースを持たないという保証が必要です。
もしもアンマネージドリソースを持ちたくなったら、SafeHandle のサブクラスでラップしてマネージドリソースに変換することをルール化しましょう。

ただし、サブクラス側で Dispose() を上書きしても、using 文で呼ばれるのはベースクラス側の Dispose() です。厳密にいえば IDisposable を実装したクラス階層の Dispose() が呼ばれます。 これでは都合が悪いので下記のいずれかの対策が必要です。

  1. サブクラス側で IDisposable を再宣言し、Dispose() を(警告CS0108 避けに new を付けて)再実装する
  2. ベースクラス側で Dispose() を virtual 宣言し、サブクラス側で override 宣言して再実装する
  3. ベースクラスを sealed 宣言して、派生を禁止する

対策1と対策2はusing文に対して期待通りに動作しますが、このような小細工を必要とするならば簡易版とは言えず、FxCopを入れるとCA1063警告が出るので素直にdisposeパターンを使う方がましです。結局のところ、簡易版として意味があるのは対策3のみです。

対策1

// ベースクラスのdispose簡易実装1.
public class MyBaseClass : IDisposable
{
    // managed resource this class uses.
    private Stream _stream;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    public void Dispose()
    {
        Console.WriteLine("Base.Dispose");
        if (!_disposed)
        {
            // TODO: Dispose managed resources here.
            _stream?.Dispose();
            // Note disposing has been done.
            _disposed = true;
        }
    }
}

// サブクラスのdispose簡易実装1.
// using文で MySubClass.Dispose() が呼ばれるように IDisposable を再宣言する.
public class MySubClass : MyBaseClass, IDisposable
{
    // managed resource this class uses.
    private Stream _stream2;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    new public void Dispose() // 警告 CS0108 避けに new を付ける.
    {
        Console.WriteLine("Sub.Dispose");
        if (!_disposed)
        {
            // TODO: Dispose managed resources here.
            _stream2?.Dispose();
            _disposed = true;

            // Call the base class implementation.
            base.Dispose();
        }
    }
}

対策2

// ベースクラスのdispose簡易実装2.
// Dispose() を virtual 宣言する
public class MyBaseClass : IDisposable
{
    // managed resource this class uses.
    private Stream _stream;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    virtual public void Dispose()
    {
        Console.WriteLine("Base.Dispose");
        if (!_disposed)
        {
            // TODO: Dispose managed resources here.
            _stream?.Dispose();
            // Note disposing has been done.
            _disposed = true;
        }
    }
}

// サブクラスのdispose簡易実装2.
// Dispose() を override 宣言する
public class MySubClass : MyBaseClass
{
    // managed resource this class uses.
    private Stream _stream2;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    override public void Dispose()
    {
        Console.WriteLine("Sub.Dispose");
        if (!_disposed)
        {
            // TODO: Dispose managed resources here.
            _stream2?.Dispose();
            _disposed = true;

            // Call the base class implementation.
            base.Dispose();
        }
    }
}

対策3

// ベースクラスのdispose簡易実装3.
// sealed 宣言して、派生を禁止する
sealed public class MyBaseClass : IDisposable
{
    // managed resource this class uses.
    private Stream _stream;
    // Track whether Dispose has been called.
    private bool _disposed = false;

    public void Dispose()
    {
        Console.WriteLine("Base.Dispose");
        if (!_disposed)
        {
            // TODO: Dispose managed resources here.
            _stream?.Dispose();
            // Note disposing has been done.
            _disposed = true;
        }
    }
}

ref構造体の場合の実装(パターンベースなusing)

C#8.0以後では、ref 構造体に限定されますが、 IDisposable を宣言しなくても Dispose() メソッドがあれば、using文でそれが呼ばれます。

using文の言語定義を最初からそのように設計しておけば、IDisposableインタフェースも、disposeパターンも要らなかった気がしますが、後の祭りです。

アンマネージドリソースもマネージドリソースも持たない場合

不要です。何もしなくて良いです。

leaveOpen と Dispose実装

Reader/Writerクラスは、一般的にコンストラクタでストリームを受け取り、それに対して加工処理を施します。
ストリームの全体をReader/Writerクラスで処理する場合は問題ありませんが、ストリームの一部をReader/Writerクラスで処理する場合は、Reader/WriterクラスのDisposeでストリームを解放すると、以後の処理が出来なくて困ります。
そこで C#標準クラスの BinaryReader ではコンストラクタにストリームを渡すときに、bool leaveOpen を同時に渡して、Disposeでストリームを解放するかどうかを指定できます。

System.IO.BinaryReader
public BinaryReader (System.IO.Stream input,
 System.Text.Encoding encoding,
 bool leaveOpen);
/*
leaveOpen:
  BinaryReader オブジェクトを破棄した後に
  ストリームを開いたままにする場合は true、
  それ以外の場合は false。
*/

leaveOpen に対応した自作クラスの Dispose 実装例

自作のReader/Writerクラスにて、同様に leaveOpen を受け取る場合の Dispose パターン実装は次のようになります。

MyReader
class MyReader : IDisposable
{
    public Stream BaseStream { get; private set; } = Stream.Null;
    public bool IsDisposed { get; private set; } = false;
    public bool IsLeaveOpen { get; private set; } = false;

    public MyReader(Stream stream, bool leaveOpen = false)
    {
       BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
       IsLeaveOpen = leaveOpen;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (IsDisposed) return;
        if (disposing)
        {
            if (!IsLeaveOpen)
            {
                BaseStream.Dispose();
            }
        }
        IsDisposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
    }  
}

leaveOpen を持つ System.IO.BinaryReader の Dispose 実装例

ちなみに C#標準クラスの BinaryReader のサンプル実装は、こうなっています。
https://referencesource.microsoft.com/#mscorlib/system/io/binaryreader.cs

System.IO.BinaryReader.Dispose(bool)
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            Stream copyOfStream = m_stream;
            m_stream = null;
            if (copyOfStream != null && !m_leaveOpen)
                copyOfStream.Close();
        }
        m_stream = null;
        m_buffer = null;
        m_decoder = null;
        m_charBytes = null;
        m_singleChar = null;
        m_charBuffer = null;
    }

頑張ってフィールドに null を設定していますが、#nullable enable な時代において、この辺りの処理は null に代わって Stream.Null を設定してゆく流れになるのかと思います。

参考資料

  1. Microsoft公式ドキュメント:IDisposable インターフェイス
  2. Microsoft公式ドキュメント:Dispose メソッドの実装, Dispose パターン
  3. Microsoft公式ドキュメント:セーフハンドルによるdisposeパターン実装例
  4. C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて
  5. [雑記] Dispose にまつわる余談
  6. パターン ベースな using
  7. 【C#】Disposeとは?
  8. [C#]イマイチ分かりにくいIDisposableの実装方法をまとめる。
144
126
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
144
126