WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

Dispose パターン (その 2)

by WebSurfer 2020年6月20日 13:32

先の記事「Dispose パターン」で、Visual Studio による Dispose パターンの自動生成機能の説明と、その機能を利用して自作カスタムクラスに Dispose パターンを実装する例を書きました。

この記事では Dispose パターンを実装済のクラスを継承してカスタムクラスを作成する際に、それに Dispose が必要な .NET のクラスのインスタンスを保持する場合、どのようなコードを書けばよいかについて述べます。

説明に ASP.NET MVC のプロジェクトを作成するテンプレートで ASP.NET Identity を実装した時に生成される AccountController の例を挙げます。自分が書いたコードで説明するより説得力があると思いますので。(笑)

Visual Studio が自動生成する Controllers/AccountController.cs のコードは以下のようになっています。この記事の説明に不要な部分は省略しています。

namespace Mvc5App2.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        private ApplicationSignInManager _signInManager;
        private ApplicationUserManager _userManager;

        // ・・・中略・・・

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_userManager != null)
                {
                    _userManager.Dispose();
                    _userManager = null;
                }

                if (_signInManager != null)
                {
                    _signInManager.Dispose();
                    _signInManager = null;
                }
            }

            base.Dispose(disposing);
        }

        // ・・・中略・・・
    }
}

AccountController の継承元 Controller クラスは IDisposable インターフェイスを継承しており Dispose パターンを実装しています。

フィールド _signInManager と _userManager には、ApplicationSignInManager と ApplicationUserManager が初期化されてそれらのオブジェクトへの参照が代入されるようにコーディングされています。

ApplicationSignInManager と ApplicationUserManager は継承元が IDisposable インターフェイスを継承しており Dispose パターンを実装しています。

AccountController には上に紹介した「Dispose パターン」の記事のような Dispose パターンのコードは実装できませんが、AccountController が Dispose されるときには _signInManager と _userManager も Dispose する必要があります。

というわけで、上のコードのように、Controller クラスが実装している Dispose(bool) メソッドをオーバーライドして _signInManager と _userManager が Dispose されるようにしています。

ASP.NET が要求の処理を終えてコントローラーをアンロードする際、自動的にコントローラーの Dispose() メソッドが実行されます。デバッガで上の Dispose(bool) メソッド内にブレークポイントを置いて実行し、AccountController のアクションメソッドを呼び出してみてください。Dispose(bool) メソッドに制御が飛んでくることで Dispose されるのが分かると思います。

(自分が試した限りですが、上の Dispose(bool) メソッドに制御が飛んでくる前のどこかで _signInManager と _userManager は Dispose されるようで、デバッガで値を調べると null になっていました。それゆえ null をチェックするコードが入っているようです)

次に、Windows Forms アプリの例を紹介します。下のコードは Visual Studio で自動生成されたフォームの .Designer.cs のコードです。(この記事の説明に不要な部分は省略しています)

namespace WindowsFormsApplication1
{
    partial class Form3
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.bindingSource1 = new BindingSource(this.components);
            this.bindingNavigator1 = new BindingNavigator(this.components);
            // ・・・中略・・・
        }

        // ・・・中略・・・
}

上のコードで Form3 は Form クラスを継承しており、Form クラスは Dispose パターンを実装しています。なので、上の AccountController の場合と同様に、Form クラスが実装している Dispose(bool) メソッドをオーバーライドし、内部で使用された Dispose が必要な .NET のクラスを Dispose するようになっています。

AccountController の場合とは違って .NET のクラスへの参照を直接 Dispose するのではなく、Container クラス(コンポーネントをカプセル化し追跡するコンテナ)にまとめて Dispose しています。

デザイナ画面で Dispose が必要なコントロールを Form にドラッグ&ドロップすると、.Designer.cs の InitializeComponent メソッド内に Container クラスを初期化し、そのコントロールを Container オブジェクトに追加するコードが自動生成されます。上のコード例では、BindingSource と BindingNavigator が Container オブジェクトに追加されています。

そのあたりの詳しい説明は @IT の記事「第4回 Visual Studio 2010のひな型コードを理解する (3/4)」が参考になると思います。

なお、デザイナ画面を使わないで自分でコードを書いた場合は、InitializeComponent メソッド内には上のようなコードは自動生成されませんので、.cs ファイルの方に自力でコードを書くことになります。具体例は別の記事「XML ファイルを DataGridView に表示」のコードを見てください。

また、IDisposable を継承しているクラスでも、全てが自動的に Container オブジェクトに追加されることはないです。例えば DataGridView クラスとか DataSet クラスがそうです。ホントに Dispose が必要かという疑問はありますが、IDisposable を継承しているクラスは使い終わったら Dispose するのが基本のようですので、上に紹介した記事のように自分でコードを書いて Container オブジェクトに追加した方がよさそうです。(初期化する前に Add しても無効のようですので注意してください)

Container オブジェクトに登録しておけば、フォームの右上の × 印アイコンをクリックするなどしてフォームを閉じる際に、自動的にフォームの Dispose() メソッドが実行され、上のコードの Dispose(bool) メソッドも実行されます。(これもブレークポイントを置いて実行してみれば、そこに制御が飛んでくることで Dispose されるのが分かると思います)

Tags: ,

.NET Framework

Dispose パターン

by WebSurfer 2019年5月31日 18:30

Visual Studio Community 2015(以下、VS2015 と書きます)には Dispose パターンに準拠したコードの骨組みを自動生成する機能が追加されています。

Dispose パターンの実装

上の画像がそれで、IDisposable とタイプすると電球マークが表示されるので、その横の▼印をクリックすると Disposeパターンを選択できるようになります。

ここで、[Dispose パターンを使ってインターフェイスを実装します]にマウスをポイントすると以下の画像(Dispose パターンの骨組みのコード)が表示され、クリックするとそのコードがクラスに挿入されます。

([... 明示的に ...]の方を選ぶと、Dispose メソッドが IDisposable.Dispose と明示的に実装されます。その場合、クラスインスタンスから Dispose には直接アクセスできませんので注意してください。明示的の使い道は調べてなくて不明です)

コードの実装

そういう機能が追加されていることを知ってました? 実は自分は最近まで知らなかったです。(汗) VS2010 にはその機能はありませんでしたので、VS2012 ~ VS2015 のどれかから追加されたようです。

Dispose パターンの実装については、Microsoft のドキュメント「Dispose メソッドの実装」などに書かれていますが、読んでもよく分かりませんでした。(笑)

(ちなみに、VS2015 で自動生成されるのは上の Microsoft のドキュメントの「Dispose パターンには 2 種類あります」以下に書いてある後者の Object.Finalize メソッドをオーバーライドする方です)

VS2015 で自動生成されたコードと、ネットの情報「確保したリソースを忘れずに解放するには?」や「C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて」などを読んで、やっと Dispose パターンを理解できたような気がします。

Dispose パターンの実装が必要なのは、アンマネージドリソースを使用するクラスのみです。アンマネージドリソースの種類で一般的なのは、ファイル、ウィンドウ、ネットワーク接続、データベース接続などのオペレーティングシステムリソースをラップしたオブジェクトです。

.NET Framework のクラスライブラリの中にも、例えば FileStream のようにファイルを開くために Windows API を呼び出してファイルハンドルを保持するというように、アンマネージドリソースを利用するものがあります。

そういうクラスは、IDisposable インターフェイスを継承して Dispose パターンを使った実装がされていて、Dispose メソッドでアンマネージドリソースを開放できるようになっているはずです。

ソースを見たわけではないので、.NET Framework のクラスライブラリの全部が全部そうなのかは分かりませんが、Dispose メソッドを実装しているクラスは内部でアンマネージドリソースを使っていると考えて、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すことを基本とすべきと思います。

自分で作るカスタムクラスの場合、マネージドリソースしか保持しない場合は Dispose パターンの実装は不要です。必要なのはアンマネージドリソースを保持する場合のみですが、それには以下のケースがあると思います。

  1. Dispose パターンを実装した .NET のクラスのインスタンスを保持している。
  2. クラス内でアンマネージドリソースを取得し、それを保持している。  

VS2015 のウィザードが生成する Dispose パターンの骨組みのコードを利用して、上記 1 および上記 1 + 2 のケースでコードを実装してみます。

上記 1 のケース

Dispose パターンを実装した .NET のクラスの例として DataSet を使いました。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;

    public DisposableSample()
    {
        myDataSet = new DataSet();
        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    
    // ・・・中略・・・
}

上記 1 + 2 のケース

アンマネージド(= GC では解放できない)リソースの例として SqlConnection で接続プールから取得する接続を使いました。接続プールはデフォルトで有効で、SqlConnection を Open すると接続プールから接続を取得してきます。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;
    private SqlConnection connection;

    public DisposableSample()
    {
        myDataSet = new DataSet();

        // アンマネージドリソースを取得
        string connString = ConfigurationManager.
                            ConnectionStrings["MyDB"].
                            ConnectionString;
        connection = new SqlConnection(connString);
        connection.Open();

        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            // アンマネージドリソースを解放
            // SqlConnection の Dispose と Close は同じ
            connection.Dispose();

            disposedValue = true;
        }
    }

    // Dispose し忘れても、ガベージコレクタが働いたときに
    // ファイナライザが呼ばれるので、そこでアンマネージド
    // リソースを解放できるよう以下のコードを実装する。
    // C++ のデストラクタの構文だが、C# ではこれによりフ
    // ァイナライザをオーバーライドすることになるらしい
    ~DisposableSample() 
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);

        // 上の Dispose(bool disposing) メソッドでアンマネー
        // ジドリソースを解放した場合はファイナライザが実行
        // されないようにする
        GC.SuppressFinalize(this);
    }

    // ・・・中略・・・
}

上のコードのアンマネージドリソースの取得で、実際にこのような形で接続を保持するケースはないのかもしれませんが、他に具体例が思いつかなかったのでこうしてみました。

プログラマが Dispose() するコードを書くのを忘れた場合でもガベージコレクタは働きます。ガベージコレクタではアンマネージドリソースは開放できませんがファイナライザは呼び出されるそうです。なので、上の ~DisposableSample() でファイナライザをオーバーライドし、その中で Dispose(false) を呼び出してアンマネージドリソースを開放するというパターンを実装するそうです。

Dispose() メソッドが呼ばれ、それから Dispose(bool disposing) メソッドが呼ばれてアンマネージドリソースを解放した場合は GC.SuppressFinalize(this) を呼び出します。 ファイナライザの実行はパフォーマンスに大きな影響を与えるそうですが、GC.SuppressFinalize メソッドを呼び出しておくと、ファイナライザの呼び出しは行われなくなります。


Dispose パターンを実装済のクラスを継承してカスタムクラスを作成し+それに上記 1 のケースで追加したオブジェクトがある場合、追加したオブジェクトを Dispose するにはどうするかについては、別の記事「Dispose パターン (その 2)」に書きます。

Tags: , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar