デリゲート

デリゲート(delegate)とは

デリゲート(delegate)とはメソッド(関数)を変数のように扱う機能です。
C言語などでは関数ポインタと呼ばれるものとほぼ同じです。

デリゲートはメソッドの処理を動的に入れ替えることができる機能です。
(delegate=委譲、委託)
あまりイメージがわかないと思うので、順に説明します。

delegateの定義


//デリゲートの定義
delegate int MyDelegate(int a, int b);

static void Main(string[] args)
{
}

デリゲートの定義はメソッドの定義に似ています。
記述する場所も通常のメソッドと同じ場所(Mainメソッドの横)です。

最初の「delegate」キーワードがデリゲートの宣言を意味します。
続いて戻り値、デリゲート名、引数の定義、と通常のメソッドの定義と同じように記述します。
最後にセミコロンを書いてデリゲートの定義は終了です。

デリゲートはメソッドの「引数とそのデータ型、戻り値のデータ型」だけを定義します。
サンプルコードでは「引数にint型をふたつ取り、int型を返すMyDelegateという名前のデリゲート型」を定義したことになります。
(仮引数名も記述しますが、この名前は重要ではありません)

デリゲートは「デリゲート型」という新しいデータ型を定義したことになります。
通常のデータ型とは異なり、メソッドを格納できる特殊なデータ型です。

デリゲートにはメソッドの処理が存在しないので、そのままでは使えません。
デリゲートを使用するには以下のようにします。


//デリゲートの定義
delegate int MyDelegate(int a, int b);

//定義したデリゲートと同じ引数、戻り値のメソッド
static int TestMethodA(int x, int y)
{
    return x + y;
}

static void Main(string[] args)
{
    //MyDelegate型の変数の使用宣言
    //同じ引数、戻り値を持つメソッドを格納
    MyDelegate md = TestMethodA;

    //古い宣言方法
    //MyDelegate md = new MyDelegate(TestMethodA);

    //TestMethodAメソッドを実行
    int num = md(3, 6);
    
    Console.WriteLine(num);
    Console.ReadLine();
}
9

まずMyDelegate型と同じ引数、戻り値を持つメソッドとしてTestMethodAを定義します。
次にMyDelegate型の変数mdを宣言し、TestMethodAを代入します。
この時、関数呼び出し演算子()は記述しません。
これを記述するとメソッドが実行することになってしまいます。

newキーワードを使用してMyDelegate型変数mdに代入する方法もあります。
どちらにしても代入できるのはMyDelegate型と同じ引数、戻り値のメソッドのみです。

これで変数mdにはTestMethodAメソッドが格納されている状態になります。
変数mdは、通常のメソッドのように関数呼び出し演算子と、必要な実引数を記述することでTestMethodAを実行することができます。
(20行目)

上記コードを少し変形して以下のようにしてみます。


delegate int MyDelegate(int a, int b);

static int TestMethodA(int x, int y)
{
    return x + y;
}

static int TestMethodB(int x, int y)
{
    return x * y;
}

static void Main(string[] args)
{
    MyDelegate md;
    if (true)
        md = TestMethodA;
    else
        md = TestMethodB;

    int num = md(3, 6);
}

同じ引数、戻り値を持つメソッドをもう一つ定義します。
MyDelegate型変数mdに代入するメソッドは条件によって変えることにします。

このようにすると、変数mdは「条件によって呼び出されるメソッドが変わる」ことになります。
これがデリゲートの特徴です。
デリゲートは、必要に応じてメソッドの処理を別のメソッドに差し替えることができる機能です。

デリゲートの用途

デリゲートの特徴は「呼び出し方はそのままで、呼び出す処理(メソッド)を変えられる」ことです。
例えばメソッドの引数にデリゲートを使用すれば、メソッド内の処理は変えずに別の処理をさせることができます。


//デリゲートの定義
delegate bool MyPredecate(int x);

//引数が10以上なら真を返すメソッド
static bool IsGTE10(int n)
{
    return n >= 10;
}

//引数が偶数なら真を返すメソッド
static bool IsEven(int n)
{
    return n % 2 == 0;
}

//配列から条件に合致する要素を抽出するメソッド
//「条件」は引数のデリゲートで指定できる
static int[] Select(int[] arr, MyPredecate predicate)
{
    //条件に合う要素の個数を数える
    int count = 0;
    foreach (var x in arr)
        if (predicate(x))
            count++;

    //戻り値となる配列を宣言
    int[] ret = new int[count];

    //戻り値となる配列に値を代入
    count = 0;
    foreach (var x in arr)
        if (predicate(x))
            ret[count++] = x;

    return ret;
}

static void Main(string[] args)
{
    int[] nums = new int[]
        { 2, 4, 7, 8, 11, 14, 18, 19 };

    int[] arr1 = Select(nums, IsGTE10);
    int[] arr2 = Select(nums, IsEven);

    foreach (var n in arr1)
        Console.Write("{0} ", n);

    Console.WriteLine();

    foreach (var n in arr2)
        Console.Write("{0} ", n);            

    Console.ReadLine();
}
11 14 18 19
2 4 8 14 18

MyPredecateは「引数にint型一つを取り、bool型を返す」デリゲートです。
IsGTE10メソッド、IsEvenメソッドの引数と戻り値はMyPredecateと同じです。
つまりMyPredecateデリゲートで利用できます。

Selectメソッドはint型配列とMyPredecateデリゲートを引数に取ります。
foreach文で配列の要素を一つずつ取り出し、引数predicateに値を渡して条件判定をしています。

Selectメソッド内でどの条件判定メソッドを使用するかは、Selectメソッドの呼び出し時に決定しています。
(43、44行目)

なお、ここでは自分でデリゲートを定義していますが、サンプルコードで使用したデリゲートと同じ(ように使える)ものは標準でC#が提供しています。
(定義済みデリゲート)


//System名前空間で定義されているdelegate
public delegate bool Predicate<T>(T obj);

//↑を使う場合
static int[] Select(int[] arr, Predicate<int> predicate)
{
    //省略
}

こちらは様々なデータ型を引数に取ることができます。
ListクラスやDictionaryクラスなどでデータ型を自由に指定できるのと同じです。

自作のデリゲートでもデータ型の指定を自由にすることはできます。
(詳しくはジェネリック参照)

定義済みデリゲート

デリゲートで使用するデリゲート型は自分で定義するのですが、C#ではよく使用されるものは最初から用意されています。
これを定義済みデリゲートといいます。
(いずれも「System」名前空間)

定義済みデリゲート 主な用途
bool Predicate<T>(T obj) 引数が条件に合致するか
TOutput Converter<TInput, out TOutput>(TInput input) TInputをTOutputに変換する
int Comparison<T>(T x, T y) xとyを比較して、xのほうが小さければ0未満を返す。
xのほうが大きければ0より大きい値を返す。
同じならば0を返す。

Actionデリゲート

Actionデリゲートはより柔軟なデータ型を取れるデリゲートです。
Actionデリゲートは「戻り値なし(void)」「引数は0~4個(データ型は任意)」というデリゲートです。
.NET Framework4.0以降では引数は16個までに拡張されています。

Funcデリゲート

Funcデリゲートは「戻り値あり(データ型は任意)」「引数は0~4個(データ型は任意)」というデリゲートです。
.NET Framework4.0以降では引数は16個までに拡張されています。
Actionデリゲートとの違いは戻り値があるかないかだけです。

どちらの場合でも引数が4個以上のメソッドはあまりなく、ましてや16個以上というメソッドはほぼないので、C#ではデリゲートを自分で定義して使用する必要はほとんどありません。


static void MethodA1() { }
static void MethodA2(int x) { }
static void MethodA3(int x, float y) { }

static int MethodF1() { return 0; }
static bool MethodF2(int x) { return false; }
static string MethodF3(int x, float y) { return "abc"; }

static void Main(string[] args)
{
    Action action1 = MethodA1;
    Action<int> action2 = MethodA2;
    Action<int, float> action3 = MethodA3;

    Func<int> func1 = MethodF1;
    Func<int, bool> func2 = MethodF2;
    Func<int, float, string> func3 = MethodF3;
}

Actionデリゲートのデータ型指定はそのまま引数のデータ型です。
引数のないメソッドの場合は指定はありません。

Funcデリゲートのデータ型の指定はまず引数のデータ型を指定し、最後に戻り値のデータ型を指定します。
引数を取らないメソッドでも戻り値はあるので、必ず一つはデータ型を指定します。

例えばFuncデリゲートを利用して、上のサンプルコードのSelectメソッドを書き直すと以下のようになります。


//もうこれは必要ない
//delegate bool MyPredecate(int x);

//配列から条件に合致する要素を抽出するメソッド
//「条件」は引数のFuncデリゲートで指定できる
static int[] Select(int[] arr, Func<int, bool> func)
{
    //条件に合う要素の個数を数える
    int count = 0;
    foreach (var x in arr)
        if (func(x))
            count++;

    //戻り値となる配列を宣言
    int[] ret = new int[count];

    //戻り値となる配列に値を代入
    count = 0;
    foreach (var x in arr)
        if (func(x))
            ret[count++] = x;

    return ret;
}

匿名メソッド

デリゲートは匿名メソッドというものを定義することもできます。
ただしこの構文は古いもので、ラムダ式が導入されてからはそちらが利用されています。


//配列から条件に合致する要素を抽出するメソッド
//「条件」は引数のFuncデリゲートで指定できる
static int[] Select(int[] arr, Func<int, bool> func)
{
    //省略
}

static void Main(string[] args)
{
    int[] nums = new int[]
            { 2, 4, 7, 8, 11, 14, 18, 19 };

    int[] arr1 = Select(
        nums,
        //匿名メソッド
        delegate(int n) 
        {
            return n >= 10;
        });
}

デリゲートを利用する場合、処理の実際の処理をするメソッドはあらかじめ定義しておかなければなりませんが、匿名メソッドを利用すれば必要な場所に直接記述することができます。
このメソッド自体は名前を持たないので「匿名」なメソッドということです。

ただし、名前を持たないので他からそのメソッドを利用することはできません。
再利用したい場合はActionやFuncデリゲートを利用する方法があります。


Func<int, bool> func = delegate (int n) { return n >= 10; };

bool b = func(5);