概要
C#のDelegate
は型です。メソッドを表す型です。
よく使われるFunc
は戻り値のあるDelegate
型、Action
は戻り値がvoid
のDelegate
型です。
Delegate
を使うためにいちいちメソッドを宣言するのが面倒なので作られたのが匿名関数です。
ラムダ式は匿名関数であり、デリゲートです。Delegate
型のインスタンス化を行います。
Delegate型とdelegate句
Delegate
型を使うときには、どんな引数や戻り値を持つメソッドなのかを事前に定義する必要があります。
delegate int Calculate(int a, int b;)
Calculate
という名前のDelegate
型を(正確には継承したクラスを)定義しました。
delegate
句はC#の文法で、Delegate
を継承したクラスを生成します。なおDelegate
を直接継承したクラスは作れません。
このままではint
2個を引数にとって何か計算して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
は戻り値がvoid
のDelegate
型、Func
はそれ以外のDelegate
型です。
Func
はジェネリックの最後の要素が戻り値です。Action
はすべて引数です。
やっていることはdelegate
でCalculate
を定義して......と同じです。
Action
やFunc
は型なのでメソッドの引数や戻り値にできます。
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
の引数a
にitems
の要素1,2,3
が順番に渡されています。
まとめ
ラムダ式は関数型の考えを取り入れたものだとかいわれることがありますが、C言語の時代にもあった関数ポインタと考え方は一緒です。新しいものでも怖いものでもないです。