デリゲート (プログラミング)

デリゲート (: delegate) とは、主にC#Visual Basic .NETなどの、.NET環境向けのプログラミング言語(.NET言語)に用意されている機能であり、参照型の一種(デリゲート型)である。

概要

デリゲートは、オブジェクトインスタンスへの参照とメソッドへの参照をペアにしてカプセル化するものである。概念としてはC言語C++関数ポインタに近いが、デリゲートは完全なオブジェクト指向である[1]型安全であるという特徴がある[2]。2002年にリリースされた.NET FrameworkではSystem.Delegateクラス[3]が定義されており、このクラスおよび派生クラスを簡潔に利用するための各種構文が.NET言語に導入された。

Object PascalDelphi)の「インスタンスのメソッドへのポインタを格納する、メソッドポインタ」と同様のものである。また、Microsoft Visual J++も、Javaと非互換のデリゲートを導入したが、.NET Frameworkのデリゲートはこれらを発展させたものである[注釈 1]

なお、C++における「クラスの非静的メンバ関数を指す関数ポインタ」は、関数呼び出し時にそのクラスのインスタンスを必要とする。デリゲートのようにオブジェクトとして扱うためにはstd::functionなどの関数オブジェクトによるカプセル化が必要となる。

デリゲートにより、メソッド単位のコンポジション (合成) が可能となる。デリゲートは主に、イベント処理での活用(コールバック処理のカスタマイズ)を想定している。Javaなどでのインターフェイスを利用したイベント処理と比べ、デリゲートによって参照されるメソッド(実体)の名前を自由に宣言できる、振る舞いをカスタマイズするために(明示的な)インターフェイスの実装やスーパークラスの継承を行なう必要がない(新たにクラスをわざわざ定義しなくてよい[注釈 2])、などの利点がある。

C#では複数のデリゲートを+, -, +=, -=演算子によって結合・分離させることもできる(マルチキャストデリゲート[4])。また、デリゲートの追加・削除と呼び出しに制約を加えて安全なコードを記述するための機構としてeventキーワードが用意されている[5] [6]eventキーワードで修飾されたデリゲート型メンバーに、外部からイベントのサブスクリプション(購読)を追加・削除する手段としては、+=演算子および-=演算子によるアクセスのみが許可される。また、eventキーワードで修飾されたデリゲート型メンバーを、クラス外部からメソッドとして呼び出すことはできない。なお、+=演算子および-=演算子の処理をカスタマイズするために、eventキーワードで修飾されたデリゲート型メンバーのaddアクセッサーとremoveアクセッサーを明示的に記述することも可能である。

そのほか、スレッドプールを利用して非同期でデリゲートを実行する機能も存在する(非同期デリゲート[7])。

Visual J++

参考

C#

C#の例を示す。まず以下では、「string型の引数を1つと、int型の戻り値を持つデリゲート」を宣言している。

delegate int SomeDelegate(string p);

コンパイラによって、System.Delegateから派生するSomeDelegate型が生成される。

次に、以下のようなメソッドを持つクラスがあったとする。

class SomeClass
{
  private string someField = "TEST:";
  public int SomeMethod(string p)
  {
    return this.someField.Length + p.Length;
  }
  public static int SomeStaticMethod(string p)
  {
    return p.Length;
  }
}

そして、以下のように(デリゲートと同じ引数と戻り値を持つ)既存の名前付きメソッドを参照するデリゲート型オブジェクトを生成することができる。

SomeDelegate del = new SomeDelegate(SomeClass.SomeStaticMethod);

C/C++の関数ポインタと異なり、静的メソッドだけでなくインスタンスメソッドをデリゲートに代入することもできる(メソッドとインスタンスのペアがカプセル化される)。

SomeClass obj = new SomeClass();
SomeDelegate del = new SomeDelegate(obj.SomeMethod);

C# 2.0以降では、匿名メソッド (anonymous method) としてインラインで記述したメソッドを参照するデリゲートを同時に定義することもできる。この書き方では、return文によって返される値がSomeDelegateデリゲートの戻り値に暗黙的に変換できない場合、エラーとなる。

SomeDelegate del = new SomeDelegate(delegate(string p) {
  return p.Length;
});

なおC# 3.0以降では、匿名メソッドの代わりにラムダ式を使って下記のように記述することもできる。

SomeDelegate del = (p) => {
  return p.Length;
};

匿名メソッドとラムダ式は併せて匿名関数 (anonymous function) と呼ばれる。匿名関数は外側の変数にアクセスすることができ、これをキャプチャと呼ぶ。匿名関数とデリゲートを用いてクロージャを実現することができる。

こうして生成したデリゲート型オブジェクトは、通常のメソッドのように直接実行することができる。

int ret = del("some string");

なお、上記は以下のInvokeメソッド呼び出しに対する糖衣構文である。Invokeメソッドはコンパイラによって自動生成される。

int ret = del.Invoke("some string");

しかし、デリゲートの真価が発揮されるのはイベントと併用した時である。イベントは、次のように宣言する。

class MyClass
{
  public event SomeDelegate SomeEvent;
}

こうして宣言したイベントには、+= 演算子と -= 演算子によって、デリゲート(イベントハンドラ)を追加したり削除したりすることができる。

MyClass obj = new MyClass();
obj.SomeEvent += new SomeDelegate(SomeEventHandler1);
obj.SomeEvent -= new SomeDelegate(SomeEventHandler1);
//obj.SomeEvent = new SomeDelegate(SomeEventHandler1); // 外部からの再代入はできないのでコンパイルエラー。
//obj.SomeEvent(); // 外部からの呼び出しはできないのでコンパイルエラー。

イベントハンドラの追加が += 演算子によって行われることから推測できるように、1つのイベントには複数のイベントハンドラを登録することができる。

次のようにしてイベントの定義されたクラスの内部からイベントを起こすと、登録したイベントハンドラがまとめて実行される。

class MyClass
{
  void ExecuteEventHandlers()
  {
    if (SomeEvent != null)
      SomeEvent("some string");
  }
}

実行される順番は登録順とは関係なく、未定義である。

なお、eventで修飾されていない通常のデリゲート型メンバーと異なり、SomeEventにはクラス外部から=演算子で直接再代入をすることはできない。また、SomeEventが定義されたクラス内からしか呼び出せない。この制約により、誤ってイベントサブスクリプションをクリアしてしまうことを防止したり、イベントハンドラーを呼び出す責任の所在を明確にしたりすることができる。

P/Invokeとデリゲート

プラットフォーム呼び出し(P/Invoke)の際は、デリゲートは既定でアンマネージ(ネイティブ)の関数ポインタにマーシャリングされる[8]

例えば下記のようなC言語関数がMyLibrary.DLLに実装されているとする。

typedef BOOL (__stdcall*TMyCallbackFuncPtr)(LPCWSTR fileName);

/* カレントディレクトリのファイルを列挙する関数。ファイルが見つかるごとにコールバック関数が呼ばれる。 */
extern __declspec(dllexport) BOOL EnumFiles(TMyCallbackFuncPtr callback);

対応するP/Invokeラッパーインターフェイスおよび呼び出しの一例は下記のようになる。

using System;
using System.Runtime.InteropServices;

static class MyPInvoker
{
    public delegate bool MyCallbackDelegate([In, MarshalAs(UnmanagedType.LPWStr)]string fileName);

    [DllImport("MyLibrary.dll")]
    public extern static bool EnumFiles(MyCallbackDelegate callback);

    private static bool MyCallbackMethod(string fileName)
    {
        Console.WriteLine(fileName);
        return true; // 列挙を続行。
    }

    private static void Test()
    {
        EnumFiles(MyCallbackMethod);
    }
}

P/Invokeでデリゲートを渡す場合は、デリゲート型のインスタンスがガベージコレクションにより回収されてしまわないように注意する必要がある[9][10]。また、コールバック関数の呼び出し規約が一致するようにしなければならない(.NET 1.1まではstdcall呼び出し規約のみが使用可能だったが、.NET 2.0以降ではUnmanagedFunctionPointerAttribute属性を明示的に指定することでcdecl呼び出し規約を使用することも可能である[11])。

その他の言語のデリゲート

D言語には関数オブジェクトがあり、型としてfunctionとdelegateがある。無名関数を作る式として関数リテラルがあり、functionとdelegateのそれぞれに対応した構文がある。関数リテラルの省略構文としてラムダ式がある。functionとdelegateの違いは、作られたスコープの環境にアクセスできるかどうかで、アクセスする場合はデリゲートである必要がある。ラムダ式では内容に応じて、デリゲートである必要がある場合はデリゲートになる。

Javaはバージョン8にてラムダ式とともにメソッド参照の機能を導入した。ただしマルチキャストデリゲートに相当する機能はない。

C++/CLIは.NETマネージ言語であり、.NETのデリゲートをサポートする。メソッド宣言にdelegateキーワードを使用することで、System::MulticastDelegateを継承した、Invokeという名前のメソッドを持つクラスが自動定義される点などは、C#とほぼ同じである[12]。ただし、C++11で追加されたラムダ式を使ってデリゲートのインスタンスを生成することはできず、gcnewを使って明示的にデリゲートのインスタンスを生成する必要があるため、C# 2.0以降と比較してコードが煩雑になる[13]

C++/CX英語版は.NETマネージ言語ではなくネイティブ言語拡張だが、デリゲートをサポートする。イベントハンドラーの割り当てに利用される。ただし、C++/CXは参照カウントベースのガベージコレクションを採用していることから、強い参照による循環参照を防ぐため、イベントハンドラーの記述にはラムダ式よりも名前付き関数を利用することが推奨されている[14]

Objective-Cは言語機能としてデリゲートを持たないが、Objective-Cを用いたイベント駆動型ソフトウェアを開発する際の基本的なデザインパターンとして「委譲」が採用されている[15]。実態はJavaのインターフェイスを利用したイベントコールバックと同じく、XxxDelegateという名前を持つプロトコル(抽象型の一種)を採用(adopt)することでイベント処理のカスタマイズを実現する。Swiftのデリゲートも同様である。

脚注

注釈

  1. ^ DelphiもJ++もC#もアンダース・ヘルスバーグによる設計である。
  2. ^ Javaではまずinterface構文によるインターフェイスやclass構文によるスーパークラスの定義が必要であり、コード量が膨れ上がりやすい。サブクラス(インターフェイス実装クラス)の記述に関しては、無名クラスやラムダ式を利用することで簡略化できるが、コンパイラによって新たにサブクラス(インターフェイス実装クラス)が定義されることに変わりはない。

出典

関連項目