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

SqlCommand の Dispose は呼ぶべきか?

by WebSurfer 2013年4月23日 13:58

結論は呼ぶべきなのですが、そのあたりのことについて調べて新発見があったので、備忘録として書いておきます。

SqlConnection クラスの Close メソッド(Dispose と機能的に同じ)を明示的に呼び出して接続を閉じることは、リソースリーク防止のため重要であることはよく知られていると思います。

詳しくは、.NETの例外処理 Part.2 を見てください。特に、そのページに書かれている、"GC の仕組みで防がれるリークはメモリリークである、という点です。実は、アプリケーションにおけるリークには大別してメモリリークとリソースリークがあり、リソースリークは GC のみで防止することができません。" という点に注目です。

では、SqlCommand クラスや SqlDataReader クラスの場合はどうでしょうか? MSDN ライブラリのサンプルコードでも Dispose メソッドを呼び出してないケースが多々見られます。SqlConnection クラスと違ってリソースリークの問題はなさそうですが、メモリリーク防止のため Dispose メソッドを呼び出す必要はないのでしょうか?

MSDN ライブラリの Dispose メソッドの実装 を見ると、"マネージリソースのみを使用する型は、ガベージコレクターによって自動的にクリアされるため、このような型で Dispose メソッドを実装しても、パフォーマンス上の利点はありません。" とのことです。(.NET 4.6 / 4.5 の記事にはその説明はありませんが同じことかと思います)

ということは、アンマネージリソースを使用していない SqlCommand などでは、Dispose メソッドを呼び出してもパフォーマンス上の利点はないということになってしまいます。

しかし、実は、Dispose メソッドには、メモリ開放の機能以外に、GC.SuppressFinalize メソッド を実装することにより、冗長なファイナライザーの呼び出しを防ぐことができるという利点があるそうです。

そのあたりは、MSDN フォーラムの Should I call Dispose on a SQLCommand object? というページで、Microsoft (MSFT) の方が "To prevent a finalizer from running, most well written Dispose implementations call a special method called GC.SuppressFinalize, which indicates to the GC that its finalizer shouldn't be run when it falls out of scope (as the Dispose method did the clean up). The component class (which remember the SqlCommand indirectly inherits from), implements such a Dispose method. Therefore to prevent the finalizer from running (and even though it does nothing in the case of SqlCommand), you should always call Dispose." と述べてられている通りだと思います。

また、Dispose メソッドの実装 の説明にも、"Dispose メソッドは、破棄するオブジェクトの SuppressFinalize メソッドを呼び出す必要があります。 SuppressFinalize を呼び出すと、オブジェクトが終了キューに置かれている場合は、そのオブジェクトの Finalize メソッドの呼び出しは行われません。Finalize メソッドの実行は、パフォーマンスに影響を与えることを覚えておいてください。" と書かれています。(.NET 4.6 / 4.5 の記事にはその説明はありませんが同じことかと思います)

という訳で、結論は、IDisposable インターフェイス を継承して Dispose メソッドを実装しているクラスは、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すべきということのようです。

そのために良く使われるのが、上に紹介した .NETの例外処理 Part.2 にある try/finally パターンや using ステートメントですね。

using ステートメントを使用すると、以下の例のようになると思います。

using(SqlConnection conn = new SqlConnection("接続文字列"))
{

  conn.Open();

  using(SqlCommand cmd = new SqlCommand("クエリ", conn))
  {
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
      if (reader != null)
      {
        while (reader.Read())
        {
          // 何らかの処置
        }
      }
    } // SqlDataReader の Dispose���注1)

  } // SqlCommand の Dispose

} // SqlConnection の Dispose(注2)

注1:Dispose メソッドは、SqlDataReader によって使用されているリソースを解放し、Close メソッドを呼び出します。

注2:SqlConnection クラスの Close メソッドと Dispose メソッドは、機能的に同じです。

------ 2014/9/25 追記 ------

クラスによってはコンストラクタに GC.SuppressFinalize メソッドが実装されており、冗長な Finalize メソッドの呼び出しを防ぐという意味では Dispose() メソッドを呼ぶ必要はないものもあります。

実は、SqlCommand クラスの場合も、現時点のソースコードを見る限り、コンストラクタに GC.SuppressFinalize メソッドが実装されています。

ただし、コンストラクタでの GC.SuppressFinalize の実装は MSDN ライブラリなどにはドキュメント化されてない(ソースコードを見ないと分からない)、ソースコードは変更される可能性がある、将来ネイティブリソースが含まれる可能性はゼロではない(ゼロに近いとは思いますが)・・・ということを考えるべきです。

なので、IDisposable を継承するクラスは Dispose() を呼ぶべきというのが基本ルールであると思っています。

------ 2015/9/11 追記 ------

リンク先の MSDN ライブラリの記事を .NET Framework 4 のものに固定しました。URL にバージョンを指定しないと .NET 4.5 / 4.6 の MSDN ライブラリにリンクされますが、その記述が .NET 4 のものかなり異なっていて、.NET 4 をベースに書いた上の記事とミスマッチが生じましたので。

Tags: ,

ADO.NET

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar