熱燗ロックのブログ このページをアンテナに追加 RSSフィード Twitter

2013-03-30

2012-03-20

「Silverlightを囲む会in東京#6」に登壇します

Silverlightを囲む会in東京#6に登壇します。

カスタムコントロールの作り方でも紹介しようかなぁ、などと思ってます。テーマが「Silverlight5新機能とSilverlightの基礎」なのにSilverlight 5のお話を入れられそうになく、そのうえ周りの講師がみんなマイクロソフトの方だったりMVPの方だったりで、とっても恐縮しております。

プレゼン内容、まだ勉強すらできてません。これから調べます。準備間に合うんだろうか。。。

2011-12-27

Windowsフォームアプリケーションでリソースリークしないために(2)

連載記事にするつもりが、第1回のみ公開して第2回は下書きに放置していた。第2回が書きかけだからだが、当時のことをほぼ完全に忘れているので書き切れそうにない。なのでエイヤッと公開しておく。このあとControl.InvokeやBeginInvokeに関する話(この辺とか)を書きたかったような気がするのだが…。

画像非同期読み込みコントロールについて

今回特に多くリソースリークしたのが画像非同期読み込みコントロールだ。本エントリでも何度も触れる(はずな)ので、その概要を説明しておく。

当該コントロール(以降「AsyncPictureBox」とする)はPictureBoxをラップしたユーザーコントロールだ *1DBBLOB値として格納されている画像を読み込むpublicメソッド(以降「BeginLoadメソッド」とする)を公開している。画像をDBから読み込むので当然、パフォーマンス上の懸念がある。そこで画像読み込みは、非メインスレッドで非同期に行う。これにより画像の読み込みが完了しなくても操作が可能になるので、ユーザーにとっての体感速度が向上する。

画像を表示する枠が表示されているのに画像の非同期読み込みが完了するまでそこに何も表示されないのは、何だか寂しい。よってその間、読み込み中だと分かるGIFアニメーション画像(こういうの)を表示する。

DBから画像を読み込もうとしたら、存在しないことがあり得る。DB内の画像はいつ削除されるか分からない(そういうシステムだ)ためだ。AsyncPictureBoxはそういった場合、画像が存在しないことを表す画像(以降「NoImage画像」とする)を表示する。その画像は静的なので、アプリケーションリソースとして埋め込む。

最後にAsyncPictureBoxは、場合によっては同時に500個程度も配置される。

動的コントロール破棄時は、Remove/Clearの後にDispose

Visual Studioのデザイナだけでコントロールの配置が事足りることはあまりない。大抵の場合データの状態などに応じて配置すべきコントロールの数などが変化するため、デザイナでは事足りない。そのような場合一般的に、コントロールのインスタンスを生成するコードを自分で書くが、そうやって生成したコントロールのことを動的コントロールと言う。

動的コントロールのライフサイクル*2はしばしば、その親コントロールのライフサイクルより短い。よくあるのが例えば、PanelがFormと同じライフサイクルで、そのPanel内に動的コントロールを配置、あるボタンが押されるとPanel内のコントロールを全てクリアし、またそのPanel内に動的コントロールを配置、といった感じだ。

Windowsフォームアプリケーションプログラミングの基礎で、コントロールの破棄時にはDisposeしなければならない。*3そこで次のようなユーティリティメソッド*4が書ける。

public static void ClearControls(this Control control)
{
    if (control == null)
    {
        throw new ArgumentNullException("control");
    }
    var controls = control.Controls.Cast<Control>().ToArray();
    control.Controls.Clear();
    foreach (var ctrl in controls)
    {
        ctrl.Dispose();
    }
}

ここでポイントなのが、DisposeをClearより後に行っている点だ。そのためにClearの前にControlsをバックアップしておくという、少々直感的でないコードになっている。なぜそうするかというと、Dispose済みのオブジェクトへの参照がないようにする、という.NETプログラミングの鉄則に則るためだ。1つのメソッド内の話なので、マルチスレッド絡みでもない限り問題が起きそうにはないが。

と、ここまで書いて説明の順番を誤ったと気づいた。この場合はDispose→Clearでも問題が起きたことがないのだった。もしかするとウィンドウハンドルがリークするのではないかと思うが、そこまでの確認をしていない。ただこの順序が非常に重要なケースがある。それを次に説明する。

ImageListをたくさん配置してはいけない

AsyncPictureBoxの初期の実装では、PictureBox以外にもう1つ、ImageListを配置していた。そしてVisual Studioのデザイナで、そのImageListにNoImage画像を格納しておいた。その結果、あまりに多くメモリを消費することになってしまった。まずはその理由を説明する。

Visual StudioのデザイナでImageListを配置して画像を読み込ませると、画像のバイナリデータが文字列*5されてRESXファイルに保存される。アプリケーションの実行時には、当該ImageListを配置したコントロールのコンストラクタ内で、RESXの文字列から実際の画像のバイナリデータに変換される。

従ってAsyncPictureBoxのコンストラクタ内では、実際にNoImage画像が表示されるかどうかによらずNoImage画像のバイナリデータがメモリ内に配置されることになる。これはあまりにメモリの無駄だ。

ではどうするかというと、アセンブリに埋め込んだ画像リソースを使う。なぜ画像リソースにすべきなのか。理由は簡単で、画像リソースなら必要になった時にだけメモリ内に配置することができる(自然にそうなる)からだ。

Visual Studioを使えば画像リソースアセンブリに埋め込むのは直感的で簡単だ。DOBON.NETさんが素晴らしい記事を書いてくれている。

Nullable<T>と3値論理

.NETのNullable<T>の比較について記憶できないのでメモ。

nullと非nullの比較

int? value1 = null;
int? value2 = 10;

だと

結果
value1 < value2false
value1 <= value2false
value1 == value2false
value1 != value2true
value1 > value2false
value1 >= value2false

nullとnullの比較

int? value1 = null;
int? value2 = null;

だと

結果
value1 < value2false
value1 <= value2false
value1 == value2true
value1 != value2false
value1 > value2false
value1 >= value2false

考察

どうやら次のような感じか。

  • 「==」や「!=」での比較は、SQLにおけるIS NULL的にちゃんと行われる。
  • 「>」や「<」などによる大小の比較は、nullが混じった時点で常にfalseを返す。

感想

試す前はSQL3値論理的なノリで、nullが混じった時点で全部falseかと思ったが、実際には違った。書いていて思ったけど3値論理関係ないですね。単なるNULLの特性の話ですね。

間違いなく言えることは、このエントリーの内容に依存したコードとか書いちゃダメ!!ちゃんとnullチェックしましょう。

*1:今思うとPictureBoxから派生したコントロールの方が良かったかもしれない。

*2インスタンスが生成されてから破棄(DisposeおよびGC)されるまでの期間

*3:正確には.NETプログラミングの基礎かもしれない。コントロールがIDisposableインターフェイスを実装しているので。

*4:拡張メソッドにした

*5:おそらくbase64か

2011-04-21

Windowsフォームアプリケーションでリソースリークしないために(1)

最近になって初めて*1、本格的なWindowsフォームアプリケーションを開発した。その中でいくつかリソースリークしやすいポイントを見つけたので記録する。

書いてみるとかなりの分量になったので、分割することにした。本エントリはその第1回。

Windowsフォームアプリケーションとは

いきなり「Windowsフォームアプリケーション」という言葉を使ったが、.NET FrameworkSystem.Windows.Forms名前空間以下に含まれる型を使って作成するデスクトップアプリケーションという意味で使った。以降もこの意味で使う。

さて、ここからが主題。

Image.Disposeを忘れない

Windowsフォームアプリケーションで画像を扱う場合、一般的にImageクラスを使う。このクラスはIDisposableインターフェイスを実装している。よってそのインスタンスを使い終わった時点で、Disposeしてやる必要がある。なお分かりやすい例としてImageクラスを挙げたが、System.Drawing名前空間オブジェクトはほとんど(全て?)同様だ。

ここまでだと当たり前に思えるが、次の場合はどうだろう。

PictureBox.Imageも要Dispose

Windowsフォームアプリケーションで画面上に画像を表示する場合、一般的にPictureBoxコントロールを使う。リンク先(DOBON.NETさん)に重要なことがサラッと書かれている。「PictureBox.Imageプロパティを使って表示した画像を消去する」のサンプルコードだ。次に引用する。

if (PictureBox1.Image != null) 
{
    PictureBox1.Image.Dispose();
    PictureBox1.Image = null;
}

PictureBox.Imageプロパティにnullを設定する前に、Disposeしているのだ。これを忘れるとリソースリークに繋がる。

画像を消去する場合だとまだ良いが、画像を差し替える場合などには忘れがちではないだろうか。画像を差し替える場合、差し替える前のPictureBox.Imageプロパティ値をDisposeしなければならない。*2これを踏まえると、次のようなユーティリティメソッド*3が書ける。

public static void SwapImage(this PictureBox pictureBox, Image newImage)
{
    if (pictureBox == null)
    {
        throw new ArgumentNullException("pictureBox");
    }
    var oldImg = pictureBox.Image;
    pictureBox.Image = newImage;
    if (oldImg != null)
    {
        oldImg.Dispose();
    }
}

画像を消去する場合はnewImage引数にnullを指定する。というのは直感的でないので、次のようなメソッドも作成しておくと良いだろう。

public static void ClearImage(this PictureBox pictureBox)
{
    SwapImage(pictureBox, null);
}

DOBON.NETさんのコードとはDisposeのタイミングが異なることに注意する。私のコードではPictureBox.Imageプロパティの上書きより後に、元のImageオブジェクトをDisposeしている。この順序は画像の消去では特に重要ではないのだが、画像の差し替えの場合に非常に重要だ。この点について次に説明する。

PictureBox.Imageの古い値のDisposeは、新しい値の設定より後で

先述のサンプルコードのSwapImageメソッドでは、PictureBox.Imageプロパティの上書きより後に、元のImageオブジェクトをDisposeしていた。これには実は意味がある。Dispose→上書きの順だと、ある条件下で次のような例外が発生してしまうことがあるのだ。

[ArgumentException: 使用されたパラメータが有効ではありません。]

場所 System.Drawing.Image.get_Width()

場所 System.Drawing.Image.get_Size()

場所 System.Windows.Forms.PictureBox.ImageRectangleFromSizeMode(PictureBoxSizeMode mode)

場所 System.Windows.Forms.PictureBox.OnPaint(PaintEventArgs pe)

場所 System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)

場所 System.Windows.Forms.Control.WmPaint(Message& m)

場所 System.Windows.Forms.Control.WndProc(Message& m)

場所 System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)

場所 System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)

場所 System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

ある条件とは何か。私が確認した範囲では、次の条件をともに満たした場合に再現した。

これら以外にも条件があるかもしれないし、これらのうちに再現性に無関係な条件があるかもしれない。しかしここで重要なのは、Dispose→上書きの順だと例外が発生してしまう場合があるということだ。条件の正確さはそれほど重要ではない。Disposeは一般的に、参照している変数があるインスタンスに対して呼び出されるべきではないから、ここでもそれに従って上書き→Disposeとすべきだろう。

PictureBoxのDispose時は、PictureBox.Imageも一緒にDispose

PictureBox.DisposeメソッドはPictureBox.Imageプロパティ値のImageオブジェクトをDisposeしない。このことはリファレンスを見ても分からない。それどころかリファレンスを見ると、全く逆のことを想像してしまうだろう。*5次にリファレンスを引用する。

disposing パラメーターが true の場合、このメソッドは、この PictureBox から参照されるすべてのマネージ オブジェクトが保持しているリソースをすべて解放します。 このメソッドは、参照される各オブジェクトの Dispose() メソッドを呼び出します。

ところでPictureBox.DisposeがPictureBox.Imageプロパティ値をDisposeしないのは、自然なことだ。PictureBoxにはDisposeして良いかが分からないからだ。PictureBox.Imageプロパティ値のImageインスタンスは、PictureBoxのDispose後も使い続けられる可能性がある。

しかし逆に、PictureBox.Disposeの時点でPictureBox.Imageプロパティ値も不要になるなら、Disposeを忘れてはならない。普通のアプリケーションなら、大抵の場合これが該当するのではないか。そのような場合のために、次のようなクラスを作成しておくと良いだろう。PictureBoxと同時にImageオブジェクトもDisposeして良い場面では、単なるPictureBoxでなくこちらを使う。

public sealed class AutoPictureBox : PictureBox
{
    protected override void Dispose(bool disposing)     // (1)
    {
        try
        {
            if (IsDisposed)
            {
                return;
            }
            if (disposing)
            {
                // マネージドリソースを解放
                this.ClearImage();                      // (2)
            }
            // アンマネージドリソースを解放
        }
        finally
        {
            base.Dispose(disposing);
        }
    }
}

(2)のClearImageメソッドは「PictureBox.Imageも要Dispose」で作成したものだ。

(1)でDisposeメソッドをオーバーライドしているが、Disposeメソッドを正しく実装するのは「知らないとできない」。私はいつも、「アンマネージ リソースをクリーンアップするための Finalize および Dispose の実装」を参考にしている。

*1:今さら感が漂うが

*2:ただし、そのImageオブジェクトを他所で使っていない場合に限る。

*3:私の好みにより拡張メソッド

*4:直接呼び出してはならない理由については次のURLなどを参照 http://blogs.msdn.com/b/nakama/archive/2009/03/30/windows.aspx

*5:このことはマイクロソフトに指摘した。

2010-09-29

IronPythonでプログラミング可能なCONFIGファイルを実装する

はじめに

IronPythonを使えば、CONFIGファイルで自由にプログラミング可能なアプリケーションを、実装できる。本エントリでは、その方法を簡単に紹介する。

続きを読む