連載
One Point .NET

デリゲート再入門

吉松 史彰
2003/07/23

マルチキャスト

 デリゲートがメソッドの呼び出しと、それによって呼び出されるメソッドを切り離したことで、メソッドの呼び出しにはさまざまな付加機能を提供することができるようになった。Microsoftでは、デリゲートに2種類の付加機能を提供している。その1つがマルチキャストである。ここでのマルチキャストとは、1度のメソッド呼び出しに対して、実際には複数のメソッドを呼び出す機能のことである。例えば次のコードを見てほしい。

using System;

delegate void ShowMessageDelegate(string msg);

class Callable {
  internal void ConcreteShowMessage(string msg) {
    Console.WriteLine(msg);
  }
}

class Caller {
  internal ShowMessageDelegate ShowMessage;

  internal void CallDelegate() {
    ShowMessage("hello, from delegate!");
  }
}

class App {
  static void Main() {
    Callable callable = new Callable();
    Caller caller = new Caller();

    caller.ShowMessage
        = new ShowMessageDelegate(callable.ConcreteShowMessage);

    Fancy fancy = new Fancy();
    caller.ShowMessage
        += new ShowMessageDelegate(fancy.ShowFancyMessage);

    caller.CallDelegate();
  }
}

class Fancy {
  internal void ShowFancyMessage(string msg) {
    System.Windows.Forms.MessageBox.Show(msg);
  }
}
デリゲートのマルチキャスト機能を使用したサンプル・プログラム

 Mainメソッドにコードが追加され、FancyクラスのShowFancyMessageメソッドが新たにデリゲートに追加登録(+=)されている。デリゲートは、登録されているメソッドのリストを管理できるようになっているため、このように+=演算子でメソッドを複数登録できるのである。このコードを実行すると、コマンドプロンプトにメッセージが表示され、メッセージボックスも表示される。このように一度の「ShowMessage("hello, from delegate!");」の呼び出しで、実際には複数のメソッドが呼び出される。

デリゲートには複数のメソッドを登録することができる

 デリゲートが持っているマルチキャスト機能では、登録されている複数のメソッドは、シーケンシャルに呼び出される。上記のコードでcallable.ConcreteShowMessageの登録とfancy.ShowFancyMessageの登録の順序を入れ替えてみると、メッセージボックスで[OK]ボタンをクリックするまではコマンドプロンプトにメッセージが表示されないのが分かるだろう。シーケンシャルでなく、複数のスレッドを使ってパラレルに呼び出しを行うような場合は、System.MulticastDelegateクラスのGetInvocationListメソッドを使って登録されているメソッドの一覧をいったん取得し、同時呼び出しを行うコードを自分で記述しなければならない。

メソッドの非同期実行

 デリゲートがもたらすもう1つの付加機能は、メソッドの非同期実行機能である。この機能は、デリゲートのBeginInvokeとEndInvokeの2つのメソッドによって実現される。以下のコードは、最初のコードを修正して、ConcreteShowMessageメソッドの実行に時間がかかるようにしたバージョンである。

using System;

delegate void ShowMessageDelegate(string msg);

class Callable {
  internal void ConcreteShowMessage(string msg) {
    System.Threading.Thread.Sleep(5 * 1000);
    Console.WriteLine(msg);
  }
}

class Caller {
  internal ShowMessageDelegate ShowMessage;

  internal void CallDelegate() {
    ShowMessage("hello, from delegate!");
  }
}

class App {
  static void Main() {
    Callable callable = new Callable();
    Caller caller = new Caller();

    caller.ShowMessage
        = new ShowMessageDelegate(callable.ConcreteShowMessage);

    caller.CallDelegate();

    Console.WriteLine("Done.");
  }
}
デリゲート経由で呼び出すメソッドの実行に時間がかかるようにしたサンプル・プログラム(太字部分を追加)
デフォルトではデリゲートの呼び出しは同期的に行われるため、デリゲート経由で呼び出すメソッドの処理が完了してからMainスレッドに制御が戻る。

 このコードを実行すると、5秒たってから「hello, from delegate!」「Done.」と連続で表示される。つまり、デリゲートの呼び出しはデフォルトでは同期的に行われるのである。

 メソッドの中には、このように実行に時間がかかるものがある。時間のかかるメソッドの実行中にほかの処理を行いつつメソッドの終了を待つことができれば便利だろう。そのような非同期実行機能を提供するのがデリゲートのもう1つの付加機能である。次のコードは、上記のコードを、非同期実行を行うように書き換えたバージョンである。

using System;

delegate void ShowMessageDelegate(string msg);

class Callable {
  internal void ConcreteShowMessage(string msg) {
    System.Threading.Thread.Sleep(5 * 1000);
    Console.WriteLine(msg);
  }
}

class Caller {
  internal ShowMessageDelegate ShowMessage;

  internal IAsyncResult CallDelegate() {
    // ShowMessage("hello, from delegate!");
    IAsyncResult ret
        = ShowMessage.BeginInvoke("hello, from delegate!",
            new AsyncCallback(Callback), ShowMessage);
    return ret;
  }

  internal void Callback(IAsyncResult ar) {
    ShowMessageDelegate usedDelegate
        = (ShowMessageDelegate)ar.AsyncState;
    usedDelegate.EndInvoke(ar);
  }
}

class App {
  static void Main() {
    Callable callable = new Callable();
    Caller caller = new Caller();

    caller.ShowMessage
        = new ShowMessageDelegate(callable.ConcreteShowMessage);

    IAsyncResult ar = caller.CallDelegate();

    while(!ar.IsCompleted) {
      System.Threading.Thread.Sleep(500);
      Console.Write(".");
    }
    Console.WriteLine("Done.");
  }
}
デリゲートの非同期実行を使用したサンプル・プログラム
デリゲートのBeginInvokeメソッドを使用することにより、デリゲート経由で呼び出すメソッドを別のスレッドで非同期に実行できる。

 CallDelegateメソッドでは、ShowMessageデリゲートを直接呼び出すのではなく、BeginInvokeメソッドを呼び出している。BeginInvokeメソッドは、共通言語ランタイムが管理しているスレッドプールにメソッド実行を依頼し、すぐに制御を戻してくる。メソッドの実行が完了していなくても、制御が戻ってくるため、コードは先に進むことができる。このとき、BeginInvokeメソッドは、現在の呼び出し状況を知っているオブジェクト(IAsyncResultインターフェイスの実装)を戻り値として戻してくるため、CallDelegateメソッドはそれを返すように変更している。

 BeginInvokeメソッドは、本来のメソッドの引数以外に2つの引数を取る。1つはAsyncCallbackデリゲートで、メソッドの実行が終了したときに呼び出されるメソッド(コールバック・メソッド)を渡すことができる。もう1つは状態オブジェクトで、ここでは呼び出しに使ったデリゲートを渡している。Callbackメソッドでは、渡されたオブジェクトからShowMessageデリゲートを復元し、BeginInvokeメソッドの対になっているEndInvokeメソッドを呼び出している。BeginInvokeとEndInvokeは必ず対で呼び出さなければならない。EndInvokeメソッドを呼び出し忘れると、メモリ・リークを引き起こす可能性がある。

 Mainメソッドのほうでは、CallDelegateメソッドを呼び出した後でwhileループを回している。ループの終了条件は、BeginInvokeメソッドの戻り値のオブジェクトのIsCompletedプロパティがtrueになるまで0.5秒ずつ処理を停止し、「.」を画面に表示するのを繰り返している。このように、時間のかかる処理を背後で実行しつつ、ユーザー・インターフェイスがフリーズしないようにするためには、非同期処理機能が必要になる。非同期処理機能は、デリゲートを利用すれば無料でついてくる機能である。

まとめ

 デリゲートは、メソッド呼び出しとメソッドの実体とを分離する機能を持つ、共通言語ランタイムの型である。デリゲートを使うと、呼び出されるメソッドの実体の決定を実行時まで遅延することができる。また、デリゲートを間に挟むことによって、メソッドのマルチキャストやメソッドの非同期実行も可能になっている。End of Article

仮想メソッド・ディスパッチとデリゲート
 C#やVB.NETでポリモーフィズムを実現するために、.NET Frameworkには仮想メソッド・ディスパッチの機能がある。この機能も、メソッドの呼び出しと、呼び出されるメソッドとを、コンパイル時ではなく実行時に結び付ける仕組みである。次のコードは仮想メソッド・ディスパッチの機能を利用している例である。

using System;

using System.Windows.Forms;

class Base {
  internal virtual void Method(string msg) {
    Console.WriteLine(msg);
  }
}

class Derived : Base {
  internal override void Method(string msg) {
    MessageBox.Show(msg);
  }
}

class App {
  static void Main() {
    Derived obj = new Derived();
    ShowMessage(obj);
  }

  static void ShowMessage(Base obj) {
    obj.Method("hello, world");
  }
}
仮想メソッド・ディスパッチの機能を利用したサンプル・プログラム

 AppクラスのShowMessageメソッドの内部では、BaseクラスのMethodメソッドを呼び出しているが、このコードを実行すると画面上にはメッセージボックスが表示される。つまり、実際に呼び出されるメソッドがDerivedクラスのMethodであることは、実行時に決定されていて、ShowMessageメソッドのコンパイル時にはそれが分からない。このような機能が、仮想メソッド・ディスパッチによって実現されている。

 この仕組みとデリゲートとの違いは、仮想メソッド・ディスパッチの機能は型の互換性を前提に成り立つ機能であるという点にある。上記のコードの場合、DerivedクラスがBaseクラスを継承しているため、両者には型の互換性がある。そのため、Baseクラスのオブジェクトを要求するShowMessageメソッドに対してDerivedクラスのオブジェクトを渡しても問題なく実行できるのである。.NET Frameworkの共通型システム(Common Type System)においては、基底クラスと派生クラスの関係、およびインターフェイスとその実装クラスの関係にある2つの型には互換性がある。

 デリゲートを使って「呼び出すメソッドを実行時に決定する」仕組みを実現するには、デリゲートを呼び出しているコードと、呼び出されるメソッドが所属しているクラスとの間に、型の互換性に関する制約は存在しない。唯一の制限は、メソッドのシグネチャが一致していることだけである。型の互換性は、.NET FrameworkのJITコンパイラが行う最適化に大きな影響を与える。メソッドのインライン化やフィールドへの直接アクセスができるかどうかを決定するために、JITコンパイラは型の互換性を考慮する。そのため、最適化と無関係な理由で型の互換性が利用されると都合が悪いこともある。デリゲートを利用すれば、JITコンパイラの最適化の仕組みを妨げることなく、「呼び出すメソッドを実行時に決定する」仕組みを実現できる。仮想メソッド・ディスパッチとデリゲートには、どちらにも一長一短がある。だからこそ、開発者に選択の機会が与えられているのである。

 
 

 INDEX
  連載 One Point .NET
  デリゲート再入門
    1.デリゲートの役割
    2.デリゲートの実体とその利用方法
  3.マルチキャストとメソッドの非同期実行
 
「連載 One Point .NET」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間