C#のデリゲートとラムダ式


概要

C#のDelegateは型です。メソッドを表す型です。
よく使われるFuncは戻り値のあるDelegate型、Actionは戻り値がvoidDelegate型です。

Delegateを使うためにいちいちメソッドを宣言するのが面倒なので作られたのが匿名関数です。
ラムダ式は匿名関数であり、デリゲートです。Delegate型のインスタンス化を行います。

Delegate型とdelegate句

Delegate型を使うときには、どんな引数や戻り値を持つメソッドなのかを事前に定義する必要があります。

delegate int Calculate(int a, int b;)

Calculateという名前のDelegate型を(正確には継承したクラスを)定義しました。
delegate句はC#の文法で、Delegateを継承したクラスを生成します。なおDelegateを直接継承したクラスは作れません。

このままではint2個を引数にとって何か計算してintを返すことしか分からない抽象的なものです。具体的にどんな計算をするか定義してあげましょう。

public void Execute()
{
    Calculate sum = getSum;
    Calculate gcd = GetGcd;
}

private static int GetSum(int a, int b)
{
    return a + b; 
}

private static int GetGcd(int a, int b)
{
    if (b == 0) return a;
    return GetGcd(b, a % b);
}

これでCalculate型の変数sumには和を計算するメソッドGetSumが、gcdには最大公約数を計算するメソッドGetGcdが定義されました。

Console.WriteLine(sum(3, 5));   // => 8
Console.WriteLine(gcd(85, 25)); // => 5

使うときはメソッドの呼び出しと一緒で引数を渡します。

ActionとFunc

Calculateという名前に意味はありません。たとえintでなくlongで計算するとしてもCalculateでしょう。そこでdelegateを使った定義を省略する方法があります。

public void Execute()
{
    Func<int, int, int> sum = GetSum;
    Func<int, int, int> gcd = GetGcd;
    Action<int> writeInfo = WriteInfo;
    writeInfo(sum(3, 5));   // => 計算結果は8です
}

private static void WriteInfo(int num)
{
    Console.WriteLine("計算結果は" + num.ToString() + "です");
}

Actionは戻り値がvoidDelegate型、Funcはそれ以外のDelegate型です。
Funcはジェネリックの最後の要素が戻り値です。Actionはすべて引数です。

やっていることはdelegateCalculateを定義して......と同じです。

ActionFuncは型なのでメソッドの引数や戻り値にできます。

public void Execute()
{
    WriteInfo(GetSum, 3, 5); // => 計算結果は8です
}

private static void WriteInfo(Func<int, int, int> calculate, int a, int b)
{
    Console.WriteLine("計算結果は" + calculate(a, b).ToString() + "です");
}

GetSumメソッドを直接渡せました。やはりやっていることは今までと同じで記法が変わっただけです。

匿名メソッド式によるメソッド宣言の省略

GetSumメソッドは引数の和を返すだけのシンプルなものでした。
これだけのためにメソッド名を考えたりしたくはないですし、使う場面と宣言の場所が離れて処理を追いにくくなってしまいます。

そこでメソッドを使う場面で直接定義する記法があります。

public void Execute()
{
    Func<int, int, int> sumFunc = delegate (int a, int b) { return a + b; };
    WriteInfo(sumFunc, 3, 5);   // => 計算結果は8です
}

ついに1行になりました。
(int a, int b)が引数、{}内がメソッドの処理を表します。
これでWriteInfoに渡している部分がメソッド宣言と近くなり、何をしているのか分かりやすくなります。

ただし現在は後述するラムダ式を使うので、匿名メソッド式を書く機会はまずないです。

ラムダ式

匿名メソッド式をさらに省略して書ける記法がラムダ式です。

public void Execute()
{
    Func<int, int, int> sumFunc = ((a, b) => a + b);
    WriteInfo(sumFunc, 3, 5);   // => 計算結果は8です
}

ラムダ式の特徴として、型推論によって引数の型やreturnを省略できます。ただしメソッド処理の内部が2行以上の場合は{}でくくってreturnを明示します。

Func<int, int, int> gcdFunc = null;     // 再帰のときは初期化しないとコンパイルエラー
gcdFunc = (a, b) =>
{
    if (b == 0) return a;
    return gcdFunc(b, a % b);
};

注意点として、ラムダ式を再帰関数として使う場合はnullなどで初期化する必要があります。でないと未割当の参照によるコンパイルエラーになります。

他にもイテレータ化ができないなどラムダ式にはいくつか制限があります。C#7でローカル関数が導入されたのはこの問題を解決するためだそうです。

また、ラムダ式はデリゲートだけでなく式木(Expression Trees)としても扱えますが、本筋から逸脱するので言及はしません。

追記

ラムダ式は内部で必要に応じたDelegate型のインスタンスを生成しています。

gcdFunc = new Func<int, int, int>((a, b) =>
{
    if (b == 0) return a;
    return gcdFunc(b, a % b);
});

このnewを省略できる記法だそうです。

LINQ

ラムダ式が書けるとなにが嬉しいのかという話になります。
よくセットで上げられるLINQを考えてみます。

public void Execute()
{
    var items = new int[] { 1, 2, 3 };
    items.Select(Add10).ToArray();  // => [11, 12, 13]
}

private static int Add10(int a)
{
    return a + 10;
}

Selectの引数はFuncなので、メソッドを別で定義してやれば渡すことができます。
でも10じゃなく100を足したくなったらそのたびにメソッドを作るんでしょうか。

ラムダ式を使うとこう書けます。

public void Execute()
{
    var items = new int[] { 1, 2, 3 };
    items.Select(a => a + 10).ToArray();  // => [11, 12, 13]
}

LINQはIEnumerable<T>型に対して定義されたメソッド群で、戻り値も(基本的に)IEnumerable<T>型です。だからメソッドチェーンで書けます。
IEnumerable<T>は順番に要素にアクセスする方法が定義されただけの集合です。

LINQは引数にメソッドを取り、元のIEnumerable<T>の各要素をメソッドに渡す、というものです。
上の例だとSum10の引数aitemsの要素1,2,3が順番に渡されています。

まとめ

ラムダ式は関数型の考えを取り入れたものだとかいわれることがありますが、C言語の時代にもあった関数ポインタと考え方は一緒です。新しいものでも怖いものでもないです。

参考

デリゲート
ローカル関数と匿名関数
【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】