デリゲートとはC++などに存在する関数ポインタに近い機能を持つもので、クラスやメソッドからのコールバック、GUIのイベントハンドラなどを実現するための機構として存在しています。 デリゲートを用いることで任意のメソッドを変数のように扱うことができるようになり、ラムダ式や匿名メソッドなども記述することができるようになっています。

ここではデリゲートの基本的なことについての解説と、デリゲートとメソッドの関わり、デリゲートの宣言など関連する構文などについて解説します。

§1 デリゲートとは

はじめにデリゲートとは何か、またデリゲートの使い方・動作について理解するため、簡単な例を使って見ていきます。 次の例は、デリゲートを使ってメソッド呼び出しを行う例です。

using System;

class Sample {
  // メッセージを表示するメソッド
  static void PrintMessage()
  {
    Console.WriteLine("Hello, world!");
  }

  // 現在時刻を表示するメソッド
  static void PrintTime()
  {
    Console.WriteLine(DateTime.Now.ToString("T"));
  }

  static void Main()
  {
    // メソッドPrintMessageを参照するAction型のデリゲートを作成
    Action print = new Action(PrintMessage);

    // デリゲートを使ってメソッドを呼び出す
    print();

    // 別のメソッドを参照するデリゲートを作成してメソッドを呼び出す
    print = new Action(PrintTime);

    print();
  }
}
実行結果
Hello, world!
14:17:34

このように、デリゲートを使うことで任意のメソッドを変数に格納して呼び出すことが出来るようになります。 デリゲートを使ったメソッドの呼び出しは、通常のメソッド呼び出しと見た目の変わりはありません。 デリゲートを使ったメソッド呼び出しの結果は、当然デリゲートが参照するメソッドの動作次第となります。

もう1つ別の例として、デリゲートを使ってコールバックを実現する例を見ていきます。 まずは次のような一定時間待機するだけの単純なコードを用意します。

using System;
using System.Threading;

class Sample {
  static void Main()
  {
    PrintTime();

    Wait(TimeSpan.FromSeconds(3.0));

    PrintTime();
  }

  // 時刻を表示するメソッド
  static void PrintTime()
  {
    Console.WriteLine(DateTime.Now.ToString("T"));
  }

  // 指定された間隔だけ待機するメソッド
  static void Wait(TimeSpan timeout)
  {
    Thread.Sleep(timeout);
  }
}
実行結果
14:17:31
14:17:34

このコードのWaitメソッドを書き換え、待機が完了したらコールバックを行うようにしてみます。 コールバックするメソッドはデリゲートであるAction型の引数callbackActionで受けとるようにします。

using System;
using System.Threading;

class Sample {
  static void Main()
  {
    PrintTime();

    // PrintTimeメソッドのデリゲートを作成
    Action callback = new Action(PrintTime);

    // 作成したデリゲートを渡して待機
    Wait(TimeSpan.FromSeconds(3.0), callback);
  }

  static void PrintTime()
  {
    Console.WriteLine(DateTime.Now.ToString("T"));
  }

  static void Wait(TimeSpan timeout, Action callbackAction)
  {
    Thread.Sleep(timeout);

    // デリゲートが表すコールバックメソッドを呼び出す
    callbackAction();
  }
}
実行結果
14:17:31
14:17:34

実行結果のとおり、Waitメソッドにおいてデリゲートを通してPrintTimeメソッドが呼び出されていることが分かると思います。

このように、デリゲートにはある特定のメソッドを参照する役割を持っています。 デリゲートを使用する場合、メソッドを呼び出す側は直接特定のメソッドを呼び出す必要はなく、デリゲートが表すメソッドを間接的に呼び出すことが出来ます。

言い換えると、デリゲートを用いることで任意のメソッドを一種の変数や引数のように扱うことが出来ます。 これにより、デリゲートを指定する側からすれば呼び出させるメソッドを自由に指定することができ、デリゲートを用いてメソッドを呼び出す側からすれば呼び出しは行うが実際にどのようなメソッドが呼び出されるかは感知しないということになります。 delegateという動詞には「委譲する、委任する」、名詞では「代表、代理人」といった訳語が用いられることを知っておくとデリゲートの概念を大雑把に把握することができると思います。

上記の点をより明確にするために、先の例を書き換え他のコールバックメソッドも呼び出すように変えてみます。 Waitメソッドには変更を加えていない点に注目してください。

using System;
using System.Threading;

class Sample {
  static void Main()
  {
    PrintTime();

    // PrintTimeをコールバックするように指定して待機
    Wait(TimeSpan.FromSeconds(3.0), new Action(PrintTime));

    // PrintMessageをコールバックするように指定して待機
    Wait(TimeSpan.FromSeconds(3.0), new Action(PrintMessage));
  }

  static void PrintTime()
  {
    Console.WriteLine(DateTime.Now.ToString("T"));
  }

  static void PrintMessage()
  {
    Console.WriteLine("done");
  }

  static void Wait(TimeSpan timeout, Action callbackAction)
  {
    Thread.Sleep(timeout);

    // デリゲートが表すコールバックメソッドを呼び出す
    callbackAction();
  }
}
実行結果
15:26:15
15:26:18
done

ここまでの例では、引数なし・戻り値なし(void)のデリゲートActionを使用していますが、デリゲートの型とメソッドの引数・戻り値が一致していれば引数や戻り値のあるメソッドもデリゲートに指定することが出来ます。 次の例では、T型の引数を一つとるジェネリックデリゲートAction<T>を使ってコールバックを行っています。

using System;
using System.Threading;

class Sample {
  static void Main()
  {
    Print("start");

    // Printをコールバックするように指定して待機
    Wait(TimeSpan.FromSeconds(3.0), new Action<string>(Print));
  }

  static void Print(string message)
  {
    Console.WriteLine(message);
  }

  static void Wait(TimeSpan timeout, Action<string> callbackAction)
  {
    Thread.Sleep(timeout);

    // デリゲートが表すコールバックメソッドに引数を指定して呼び出す
    callbackAction("done");
  }
}
実行結果
start
done

なお、ここまでの例で使用してきたActionデリゲートは.NET Frameworkに用意されている汎用的なデリゲートですが、デリゲート型を独自に宣言することもできます。

さらに、ここまでの例では単一のメソッドを参照するデリゲートを使用してきました(シングルキャスト)。 いくつかのデリゲートを連結して一つのデリゲートを作成し、複数のメソッドを呼び出すようにすることも出来ます(マルチキャスト)。



§2 デリゲートとメソッド

メソッドからデリゲートをするには、メソッドとデリゲートのシグネチャ(signature、引数と戻り値の型の組み合わせ)が一致している必要があります。 関数ポインタとは異なり、デリゲートはタイプセーフであり、シグネチャが一致していないメソッドからデリゲートを作成することは出来ません。

先の例で使用したActionデリゲートは引数なし・戻り値なし(void)のメソッドを表すデリゲートです。 これと異なるシグネチャのメソッドからはActionデリゲートを作成することは出来ません。

using System;

class Sample {
  // 引数なし、戻り値なしのメソッド
  static void Method1()
  {
  }

  // 引数にstring型を取り、戻り値なしのメソッド
  static void Method2(string arg)
  {
  }

  // 引数なし、int型の戻り値を持つメソッド
  static int Method3()
  {
    return 0;
  }

  static void Main()
  {
    Action a1 = new Action(Method1);
    // Action a2 = new Action(Method2); 戻り値が一致しないため、コンパイルエラーとなる
    // Action a3 = new Action(Method3); 引数が一致しないため、コンパイルエラーとなる
  }
}

また、静的(共有)メソッドだけでなく、インスタンスメソッドを設定することも出来ます。 デリゲートは、メソッドを呼び出す際にインスタンスを区別して呼び出します。

インスタンスメソッド・静的メソッドを参照するデリゲートを呼び出す例
using System;

class TestClass {
  private string message;

  public TestClass(string message)
  {
    this.message = message;
  }

  // インスタンスメソッド
  public void Print()
  {
    Console.WriteLine(message);
  }

  // 静的メソッド
  public static void PrintMessage()
  {
    Console.WriteLine("Hello, world!");
  }
}

class Sample {
  static void Main()
  {
    TestClass i1 = new TestClass("instance #1");
    TestClass i2 = new TestClass("instance #2");

    Action a1 = new Action(i1.Print); // インスタンスi1のPrintメソッド
    Action a2 = new Action(i2.Print); // インスタンスi2のPrintメソッド
    Action a3 = new Action(TestClass.PrintMessage); // クラスTestClassのPrintメソッド

    a1();
    a2();
    a3();
  }
}
実行結果
instance#1
instance#2
Hello, world!

さらに、オーバーライドされたメソッドや、(デリゲートが作成出来る場合は)非パブリックなメソッドでも呼び出すことが出来ます。

オーバーライドされたメソッドを参照するデリゲートを呼び出す例
using System;

class BaseClass {
  public virtual void Print()
  {
    Console.WriteLine("BaseClass.Print");
  }
}

class DerivedClass : BaseClass {
  public override void Print()
  {
    Console.WriteLine("DerivedClass.Print");
  }
}

class Sample {
  static void Main()
  {
    BaseClass i1 = new BaseClass();
    DerivedClass i2 = new DerivedClass();
    BaseClass i3 = i2;

    Action a1 = new Action(i1.Print);
    Action a2 = new Action(i2.Print);
    Action a3 = new Action(i3.Print);

    a1();
    a2();
    a3();
  }
}
実行結果
BaseClass.Print
DerivedClass.Print
DerivedClass.Print
プライベートメソッドを参照するデリゲートを呼び出す例
using System;

class TestClass {
  private void Print()
  {
    Console.WriteLine("private method");
  }

  public Action GetDelegate()
  {
    // プライベートなメソッドを参照するデリゲートを作成して返す
    return new Action(Print);
  }
}

class Sample {
  static void Main()
  {
    TestClass i = new TestClass();

    Action a = i.GetDelegate();

    a();
  }
}
実行結果
private method

なお、デリゲートのMethodプロパティやTargetプロパティを参照することにより、デリゲートが実際に呼び出すメソッドやインスタンスを知ることが出来ます。

§3 デリゲートと構文

メソッドからデリゲートのインスタンスを作成するには、ここまでの例で挙げてきたようにデリゲートのコンストラクタを使う他にも、言語によって用意されている構文を使うことが出来ます。 C#ではキャストや単純な代入、VBではAddressOf演算子を使うことでコンストラクタを呼び出さなくてもデリゲートを作成することが出来ます。 無論、デリゲートの型とメソッドのシグネチャが一致していなければコンパイルエラーとなります。

using System;

class Sample {
  static void Method()
  {
    Console.WriteLine("Hello, world!");
  }

  static void Main()
  {
    // 以下のいずれも上記のMethodを参照するデリゲートを作成する
    Action a1 = new Action(Method); // コンストラクタにメソッドを指定してデリゲートを作成
    Action a2 = (Action)Method; // メソッドからキャストしてデリゲートを作成
    Action a3 = Method; // 単にメソッドを代入してデリゲートを作成

    // 作成したデリゲートの呼び出し
    a1();
    a2();
    a3();
  }
}

§3.1 匿名メソッドを使ったデリゲートの作成

匿名メソッド(C# 2.0以降)の構文を使う事によってもデリゲートを作成することができます。

匿名メソッドを参照するデリゲートを作成する(C# 2.0以降)
using System;

class Sample {
  static void Main()
  {
    // 引数と戻り値のない匿名メソッドからデリゲートを作成する
    Action a = delegate { Console.WriteLine("Hello, world!"); };
    // a = void AnonymousMethod() { Console.WriteLine("Hello, world!"); }

    a();

    // 引数と戻り値がある匿名メソッドからデリゲートを作成する
    Func<int, double> f = delegate(int r) { return Math.PI * (r * r); };
    // f = double AnonymousMethod(int r) { return Math.PI * (r * r); }

    double ret = f(3);

    Console.WriteLine(ret);
  }
}
実行結果
Hello, world!
28.2743338823081

上記コード中においてコメントアウトしてあるコードは、匿名メソッドとの比較のために通常のメソッドに似せた擬似コードを記述したものです。 実際にはコンパイルできません。

§3.2 ラムダ式を使ったデリゲートの作成

ラムダ式(C# 3.0VB10)の構文を使う事によってもデリゲートを作成することができます。

ラムダ式を参照するデリゲートを作成する(C# 3.0以降)
using System;

class Sample {
  static void Main()
  {
    // 引数と戻り値のないラムダ式からデリゲートを作成する
    Action a = () => Console.WriteLine("Hello, world!");
    // a = void lambda() { Console.WriteLine("Hello, world!"); }

    a();

    // 引数と戻り値があるラムダ式からデリゲートを作成する
    Func<int, double> f = (r) => Math.PI * (r * r);
    // f = double lambda(int r) { return Math.PI * (r * r); }

    double ret = f(3);

    Console.WriteLine(ret);
  }
}

上記コード中においてコメントアウトしてあるコードは、ラムダ式との比較のために通常のメソッドに似せた擬似コードを記述したものです。 実際にはコンパイルできません。

ラムダ式を参照するデリゲートを作成する(VB10以降・複数行形式)
Imports System

Class Sample
  Shared Sub Main()
    ' 引数と戻り値がないラムダ式からデリゲートを作成する
    Dim a As Action = Sub()
      Console.WriteLine("Hello, world!")
    End Sub

    a()

    ' 引数と戻り値があるラムダ式からデリゲートを作成する
    Dim f As Func(Of Integer, Double) = Function(r As Integer)
      Return Math.PI * (r * r)
    End Function

    Dim ret As Double = f(3)

    Console.WriteLine(ret)
  End Sub
End Class
ラムダ式を参照するデリゲートを作成する(VB10以降・単一行形式)
Imports System

Class Sample
  Shared Sub Main()
    ' 引数と戻り値がないラムダ式からデリゲートを作成する
    Dim a As Action = Sub() Console.WriteLine("Hello, world!")

    a()

    ' 引数と戻り値があるラムダ式からデリゲートを作成する
    Dim f As Func(Of Integer, Double) = Function(r As Integer) Math.PI * (r * r)

    Dim ret As Double = f(3)

    Console.WriteLine(ret)
  End Sub
End Class

上記のコードはいずれも等価なもので、以下のような実行結果となります。

実行結果
Hello, world!
28.2743338823081

§4 デリゲートの宣言

.NET Frameworkでは多くのデリゲート型が用意されていますが、独自に宣言して使用することも出来ます。 C#ではdelegateキーワード、VBではDelegateステートメントを使って宣言します。 いずれも実体を持たないメソッドの宣言のような記述になっています。

using System;

// 引数なし、戻り値なしのメソッドを表すデリゲート
public delegate void MyDelegate1();

// int型の引数を一つ取り、戻り値なしのメソッドを表すデリゲート
internal delegate void MyDelegate2(int arg);

// string型の引数を一つ取り、int型の戻り値を返すメソッドを表すデリゲート
delegate int MyDelegate3(string arg);

class Sample {
  static void Main()
  {
    MyDelegate1 m1 = new MyDelegate1(PrintMessage);
    MyDelegate2 m2 = new MyDelegate2(PrintMessage);
    MyDelegate3 m3 = new MyDelegate3(GetLength);

    m1();

    m2(6);

    int ret = m3("Hello, world!");

    Console.WriteLine(ret);
  }

  static void PrintMessage()
  {
    Console.WriteLine("Hello, world!");
  }

  static void PrintMessage(int count)
  {
    for (int i = 0; i < count; i++) {
      Console.Write("Hello! ");
    }

    Console.WriteLine();
  }

  static int GetLength(string str)
  {
    return str.Length;
  }
}
実行結果
Hello, world!
Hello! Hello! Hello! Hello! Hello! Hello! 
13

なお、デリゲート型に対するアクセス修飾子はあくまでデリゲート型に対して適用されるもので、デリゲートが表すメソッドのアクセス修飾子とは無関係です。

§4.1 汎用的なデリゲート

多くの場合、独自にデリゲート型を宣言するよりも、.NET Frameworkで用意されているデリゲート型を使うことになります。 以下は、よく使われるデリゲート・汎用的なデリゲートの一例です。

.NET Frameworkで用意されているデリゲート型
名前空間 デリゲート型 主な用途
System AsyncCallback 非同期呼び出しが完了した場合に呼び出されるコールバックメソッドを指定するために用いられる。
Comparison<T> コレクション型のクラスで二つのT型の値の大小関係を比較するメソッドを指定するために用いられる。

例: 大小関係の定義と比較 §.Comparison<T>基本型のソートと昇順・降順でのソート §.Sort + Comparison<T>ジェネリックなソートアルゴリズムの実装
Converter<TInput, TOutput> コレクション型のクラスでTInput型の値をTOutput型に変換するメソッドを指定するために用いられる。

例: 配列操作 §.全要素の変換 (ConvertAll)ジェネリックコレクション(1) List §.型変換・全要素の変換 (ConvertAll)ユーザ定義の型変換 §.Converter<TInput, TOutput>
Predicate<T> コレクション型のクラスでT型の値に対して真偽値を返す述語となるメソッドを指定するために用いられる。

例: 配列操作 §.述語を使った検索 (Find, Exists, etc.)ジェネリックコレクション(1) List §.述語(Predicate)を用いた検索
EventHandler
EventHandler<TEventArgs>
各種イベントのハンドラとなるメソッドを指定するために用いられる。
Action
Action<T>
Action<T1, T2>
Action<T1, T2, T3>など
戻り値のないメソッドを表すデリゲート。 渡された引数に応じて任意の処理を行うメソッドを指定するために用いられる。
Func<TResult>
Func<T, TResult>
Func<T1, T2, TResult>
Func<T1, T2, T3, TResult>など
TResult型の戻り値を返すメソッドを表すデリゲート。 渡された引数に応じて任意の処理を行った結果を返すメソッドを指定するために用いられる。
System.Threading ThreadStart
ParameterizedThreadStart
Threadクラスでスレッドを作成する場合に、スレッドとして起動するメソッドを指定するために用いられる。
WaitCallback ThreadPoolクラスでスレッドプールを使って別スレッドで動作させるメソッドを指定するために用いられる。
System.Net.Security RemoteCertificateValidationCallback SslStreamクラスでリモートサーバから受信したSSL証明書の妥当性を検証するメソッドを指定するために用いられる。
System.Text.RegularExpressions MatchEvaluator Regexクラスで正規表現にマッチする箇所が見つかる度にコールバックされ、別の文字列に置き換えるメソッドを指定するために用いられる。

例: 正規表現によるパターンマッチングと文字列操作 §.マッチ箇所の置換 (Regex.Replace)
System.Xml.Schema ValidationEventHandler XmlReaderクラスXmlValidatingReaderクラスでXMLスキーマによるXML文書の検証でエラーや警告が発生した場合にコールバックされるメソッドを指定するために用いられる。

以下はList<T>クラスといくつかのデリゲートを使った例です。 この例で使用しているメソッドについてはList<T>についての解説を参照してください。

using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    List<string> list = new List<string>() {
      "Alice",
      "Bob",
      "Charlie",
      "Dave",
      "Eve",
    };

    // Comparison<string>を指定してソート
    list.Sort(CompareByLength);

    // Converter<string, string>を指定して変換
    List<string> converted = list.ConvertAll(PrependLength);

    // Action<string>を指定して列挙
    converted.ForEach(Print);
  }

  static int CompareByLength(string x, string y)
  {
    return x.Length - y.Length;
  }

  static string PrependLength(string str)
  {
    return string.Concat(str.Length.ToString(), ":", str);
  }

  static void Print(string str)
  {
    Console.WriteLine(str);
  }
}
実行結果
3:Eve
3:Bob
4:Dave
5:Alice
7:Charlie