2006 年 7 月 7 日 19 時 51 分

実装の分割 #2: デリゲート


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


今日は昨日のステップをもう少し進めてみる。

switch の中が明確になり、
果たすべき役割が複数の関数に分解された。

GetValue メソッドの一部を引用する。

    switch (pscd.pwszExt.ToLower()) {

    case ".lnk":
        return GetShortcutTarget(pscd.wszFile);

    case ".url":
        return GetInternetShortcutTarget(pscd.wszFile);

    default:
        return GetOtherTarget();

    }

コードを関数に移動しただけなので、
基本的には何も変わっていない。
コードを単純にルーチンに分割するだけなら、
これ以上は分解できないほどシンプルになった。

ここで、少し視点を変えてみよう。
「見た目」がすっきりしたので見えてくることがある。
よく見ると、switch 内のコードは、
全て「単一のメソッド呼び出し」となっている。

この「メソッドを呼び出す」という意味で見れば、
switch の各 case は共通の処理をしているということだ。
この「呼び出し」を switch の外に出せないだろうか。
つまり、「メソッド呼び出しを集約する」ことを考える。

case や default の中ではメソッドを呼び出さず、
「後で呼び出す予定」としてメソッドを記憶しておく。
そして、switch の後で、呼び出しを行うのだ。


C# には、デリゲートと呼ばれる特殊な変数があり、
この変数には、メソッドへの参照を格納することができる。
メソッドを呼んで戻り値を格納するのではない。

ただ、通常変数に型があるのと同じように、
デリゲート変数にも型がある。
デリゲート変数が格納できるのはメソッドなので、
言い換えれば「メソッドに型がある」ということだ。

メソッドの型とは、メソッドのシグネチャのことである。
シグネチャとは、戻り値と引数の型の組み合わせの事だ。

百聞は一見に如かず。コードを書いて説明しよう。

まず、デリゲートを宣言する。デリゲートとは
「特定の型のメソッドへの参照を保持」できる型であり、
enum や class などと同じ空間で宣言することができる。

    delegate string LinkTargetHandler(string path);

書き方はメソッドの定義に似ている。
違いは、delegate というキーワードが入ることだ。
通常のメソッドだと、メソッド名のように見える部分が、
デリゲート宣言の場合、デリゲート型の名前となる。

上記では戻り値に string を返し、引数に string を取る、
シグネチャ持つメソッドを格納できる、
LinkTargetHandler という名前の型を定義した。
デリゲートには Handler を末尾につけるのが習慣のようだ。

さて、このデリゲートは型なので、
変数のように扱うことができる。
(暗黙に Delegate クラスから派生した参照型となる)

    LinkTargetHandler hander = null;

そして、このデリゲート型のインスタンスを作成する。

    hander = new LinkTargetHandler(GetShortcutTarget);

LinkTargetHandler のコンストラクタは、
デリゲートと同じシグネチャを持つ、
「メソッドへの参照」を引数として受け取る。

ポイントは、「GetShortcutTarget」だ。
括弧が付いてないのでこれはメソッド呼び出しではなく、
メソッド自身への参照を指す。
これによって、LinkTargetHandler インスタンスに、
GetShortcutTarget メソッドへの参照を渡す。

デリゲート経由でメソッドを呼び出すのは簡単である。

    string target = hander("引数");

デリゲート変数に括弧をつけて呼び出せば、
デリゲート変数が参照するメソッドが呼び出される。

ではこれらを使って switch 文を書き換えてみよう。

デリゲートには型があるので、
switch 内で呼び出してるメソッドの型は
共通化しなければならない。
そこで、GetOtherTarget 型に余分な引数を追加し、
メソッドの型をあわせることにしよう。

    static private string GetOtherTarget(string path)

そして、デリゲートを宣言する。
LinkTargetColumn でしか使わないので、
LinkTargetColumn のフィールドと同じ階層で宣言しよう。
private アクセス修飾子をつけること。

    private delegate string LinkTargetHandler(string path);

そして、switch 文の中を書き換える。

    // デリゲートを用意
    LinkTargetHandler hander = null;

    // 拡張子を調べる
    switch (pscd.pwszExt.ToLower()) {

    case ".lnk":
        hander = new LinkTargetHandler(GetShortcutTarget);
        break;

    case ".url":
        hander = new LinkTargetHandler(GetInternetShortcutTarget);
        break;

    default:
        hander = new LinkTargetHandler(GetOtherTarget);
        break;
           
    }

    return hander(pscd.wszFile);

どうだろうか。はっきり言って、手間が増えただけだと
感じるかもしれない。実際、俺もそう思う。

しかしながら、呼び出しを一箇所に集約することができた。
実際の処理が行われるのは、メソッド呼び出した時であり、
switch 文の中ではロジック自体は実行されない。
これは次のステップに進むにあたって重要な意味をもつのだ。

デリゲートは、C の関数ポインタと異なり、
関数への参照を直接格納する値型ではない。
関数への参照を内部に格納しているクラス型であり、
決して単純なクラスではないのだ。

デリゲートはまだまだ高度な機能を持っている
静的メソッドだけでなく、メンバメソッドを呼んだり、
複数のメソッドを連鎖的に呼び出したりする機能も持つ。
このあたりは、イベントやリスナーといった、
UI 系のプログラミングで多用されている。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.