LoginSignup
3
2

More than 3 years have passed since last update.

【C#】継承できるクラスでDisposeパターンを実装するときは、とりあえずFinalizeも実装すべき?

Last updated at Posted at 2020-06-03

はじめに

IDisposableなんて別にpublic void Dispose()を実装するだけでも事足りることばかり。でもどうせならDisposeパターンなんてより丁寧に書くための物があるんだから使いたくなるのが開発者の心情というもの。と言うわけでIDisposableを使うときの個人的なメモ。
「より安全で丁寧なコードを書くにはどうしたら良いだろう?」という考え方が、以前書いた「【Windows/C#】なるべく丁寧にDllImportを使う」に通じるものがあるかも・・・1

Disposeパターン

C#でよくリソースを破棄するために使うIDisposableインターフェイス。
それを丁寧に扱うためのテンプレートとして有名なDisposeパターン
Visual Studio 2019だとclass A : IDisposableと打ち込んで、右クリックメニューから「クイックアクションとリファクタリング...」から「破棄パターン2を使ってインターフェイスを実装します」を押すと自動的に生成してくれるアレ。

class A : IDisposable
{
    private bool disposedValue;

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: マネージド状態を破棄します (マネージド オブジェクト)
            }

            // TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
            // TODO: 大きなフィールドを null に設定します
            disposedValue = true;
        }
    }

    // // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
    // ~A()
    // {
    //     // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

ファイナライザ いる?いらない?

自動生成されるコードでは、ファイナライザがコメントアウトされていて「アンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします」と書いてある。
またDisposeパターンの正しい実装を求めてグーグル先生に尋ねてみたけど、どの情報も「アンマネージドリソースを使う場合はファイナライザを実装する」みたいな書き方になってた。
本当にそれで安全なのかな?

継承先でアンマネージドリソースが使われると...?

protected virtual void Dispose(bool disposing)

virtualを付けているくらいだから継承できるはず。
とすると、そのクラスでアンマネージドリソースを使わなかったとしても、継承先で使われる可能性は十分あり得るのでは・・・?
もしその時に継承元がファイナライザを実装していないとどうなるのかな?

継承元のファイナライザが無いとどうなる?

まずファイナライザを実装した場合を実験

継承元のAクラス。

class A : IDisposable
{
    protected virtual void Dispose(bool disposing)
    {
        Console.WriteLine($"A.Dispose({disposing})");
    }

    // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
    ~A()
    {
        Console.WriteLine($"A.~A()");

        // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
        Dispose(disposing: false);
    }

    public void Dispose()
    {
        Console.WriteLine($"A.Dispose()");

        // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

継承先のBクラス。

class B : A
{
    protected override void Dispose(bool disposing)
    {
        Console.WriteLine($"B.Dispose({disposing})");

        base.Dispose(disposing);
    }
}

あと実験用のProgram.cs。

class Program
{
    static void Test()
    {
        B b = new B();
    }

    static void Main(string[] args)
    {
        Test();
        GC.Collect();
    }
}

単純にDisposeとファイナライザを呼び出したときにコンソール出力するだけのクラス。
それをわざとDisposeをすっぽかしてみる。

結果

A.~A()
B.Dispose(False)
A.Dispose(False)

当然BクラスのDisposeFalseで呼ばれている。
ちなみにGC.Collect()を呼ばないと、ファイナライザを実装してても呼ばれなかった。

次にファイナライザを実装しなかった場合を実験

アンマネージドリソースを使ってないからとAクラスのファイナライザは実装しない。

class A : IDisposable
{
    protected virtual void Dispose(bool disposing)
    {
        Console.WriteLine($"A.Dispose({disposing})");
    }

    // // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
    // ~A()
    // {
    //     // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        Console.WriteLine($"A.Dispose()");

        // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

継承先のBクラスでは、ファイナライザでDispose(false)を呼んでくれると信じて、アンマネージドリソースの破棄処理を実装したとする。

class B : A
{
    protected override void Dispose(bool disposing)
    {
        Console.WriteLine($"B.Dispose({disposing})");

        if (disposing)
        {
            Console.WriteLine($"マネージド リソースを破棄!");
        }

        Console.WriteLine($"アンマネージド リソースを破棄したい!");

        base.Dispose(disposing);
    }
}

そしてさっきと同じように実行。

結果


しーん・・・
やはり実行されなかった。

どうすべきか?

少なくともsealedが付いていないクラス3IDisposableを実装するときは、
「継承してもアンマネージドリソースを使うわけがないクラス」
「そもそも継承する予定のないクラス」
「Disposeは必ず呼ぶから大丈夫」
色々あるだろうけど、別にファイナライザを実装しても行数が増える程度で特に問題はないので、なにも考えずにファイナライザのコメントを解除して実装してしまうのが1番安全な気がする。
作成当時は「有り得ない」と思ってても、時間が経ってから継承してアンマネージドリソースを使うかもしれない。

もちろん「アンマネージドリソースを使った側でファイナライザを実装する」というのもアリかもしれない。

class B : A
{
    ~B()
    {
        Dispose(disposing: false);
    }

    protected override void Dispose(bool disposing)
    {
        Console.WriteLine($"B.Dispose({disposing})");

        if (disposing)
        {
            Console.WriteLine($"マネージド リソースを破棄!");
        }

        Console.WriteLine($"アンマネージド リソースを破棄!");

        base.Dispose(disposing);
    }
}

むしろ「アンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします」という説明的には、こっちの方が正しい気さえする。
でも継承先でDisposeをオーバライドしてもIDisposableのテンプレートみたいにファイナライザは自動生成されないし、絶っっ対にファイナライザの実装を忘れると思う。
なによりいちいち継承先で実装するのは面倒臭い。

というわけで、IDisposableDisposeパターンで実装するときは、自動生成のコメントに惑わされず常にファイナライザは実装していこうかなーって話でした。

余談

自動生成されるDisposeパターンのコードがいつの間にか微妙に変わってる?

public void Dispose()
{
    // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
}

なんで引数にdisposing:をわざわざ付けるんだろう・・・
以前はGC.SuppressFinalize(this);もコメントアウトされてて、アンマネージドリソースを使う場合のみ呼び出す形だったような・・・


  1. 書き出しを合わせてみたけど、シリーズ化する予定はない模様 

  2. 破棄パターン(笑)昔は「Disposeパターン」と表記されてたのに機械翻訳でこうなってしまったのかな・・・ 

  3. 別にsealedとか気にせずファイナライザを実装して良い気もする 

3
2
0

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
3
2