デリゲート~ラムダ式の歴史を辿って
デリゲート(C# 1.0)
そもそもデリゲート(delegate-直訳すると委任・委託・委譲という意味)とはその名前の通り、処理の委譲を支援するための仕組みとなります。
委譲とは大辞林によると「権限などを他に任せて譲ること」となっています。
たとえば、複数店舗を展開しているスーパーがあったとします。
本部は全店舗で実施する予定のセールの説明をするため、各店舗の店長相当の人を会議に招集します。
このとき、実際にはセールが滞りなく実施できればいいため、店長である必要はなく、セールをきちんと推進する権限を持った人であればだれでもよい訳です。
つまり、このように委譲する場合にはきちんと「委譲することができる人」の「条件の定義」が必要となる訳です。
C#においても同じように考えることができて、デリゲートという仕組みでは、まずは処理を委譲することができる関数(メソッド)の条件を定義する必要があります。
そこで、C#のデリゲートでは、まずメソッドの参照を行うために利用する条件を「メソッドのシグネチャ」、つまりメソッドの引数や戻り値を定義することで設定します。 そして定義されたデリゲートはC#では「型」(厳密には参照型)として機能します。
それでは定義方法です。定義は「delegate」という予約語を用いて「メソッド」の「引数」や「戻り値」などを指定します。
前述のとおり、デリゲートは型として定義することになるので、定義したデリゲートを用いることで、メソッドを格納するための「変数宣言」を行うことができる訳ですね。
以下のサンプルでは「CalculateMethod」という名前のデリゲートを定義しています。
ここではintの戻り値を持ち、2つのint型の引数を持つデリゲートはこんな感じで定義します。
// デリゲートを定義 public delegate int CalculateMethod(int x, int y);
次に宣言したdelegateを用いて変数宣言を行って、それを利用するところ確認してみましょう。
デリゲートで変数宣言した変数には定義した「メソッドのシグネチャ」と同じシグネチャを持つ「メソッドそのもの」を代入することが可能です。
そして、代入した変数を用いてメソッドを呼び出すことが可能になります。
以下では先ほど定義した「CalculateMethod」を用いて変数宣言を行い、その変数に代入・呼び出しを行ってみたコードです。
class Sample
{
// デリゲートに代入するためのメソッド(足し算)
public intAdd(intx,inty)
{
returnx + y;
}
// デリゲートに代入するためのメソッド(引き算)
public intSub(intx,inty)
{
returnx – y;
}
}
classProgram
{
// デリゲートを定義
public delegate intCalculateMethod(intx,inty);
static voidMain(string[] args)
{
varmain =newSample();
// Delegateに代入
CalculateMethodcalc =newCalculateMethod(main.Add);
Console.WriteLine(calc(10, 20));
// Delegateに代入
calc =newCalculateMethod(main.Sub);
Console.WriteLine(calc(10, 20));
Console.ReadLine();
}
}
ご覧のように、デリゲートは参照型のため、「newキーワード」によってインスタンス化を行い、このコンストラクタに「実体となるメソッド」を指定することで代入することができます。
実行結果は以下の通りです。
このような手順によってメソッドの呼出を委譲することができる訳です。
ご覧のとおり、CやC++でいうところの関数のポインタに似ていますよね。
これによって、メソッド自体を変数として扱えるため、デリゲート自体をプロパティにしたり、メソッドの引数や戻り値に設定することもできるため、外部にあるオブジェクトのメソッドの呼出を簡単に行えるようになりました。
●複数メソッドの代入
そして、delegateのもうひとつの特徴はdelegateで宣言された変数には「複数のメソッド」を代入することができます。
複数メソッドの代入を行う場合は、「同じシグネチャを持つメソッド」を連結し、変数に代入します。
この代入された変数を利用すことで「連結されたメソッド」をまとめて呼び出すことができます。
ただし、このときの連結されたメソッドの呼出順序については仕様で保証されていないので注意してください。
もともとC#のdelegateはMulticastDelegateクラスから派生することによって実現されています。
そのクラス名からもわかるように、MulticastDelegateでは指定されたメソッドを「呼出リスト」と呼ばれる形で管理されており、1つ以上指定されたメソッドを順次呼び出す機能をもっています。
class Sample
{
// デリゲートに代入するためのメソッド(足し算)
public voidAdd(intx,inty)
{
Console.WriteLine(x + y);
}
// デリゲートに代入するためのメソッド(引き算)
public voidSub(intx,inty)
{
Console.WriteLine(x – y);
}
}
classProgram
{
// デリゲートを定義
public delegate void CalculateMethod(intx,inty);
static void Main(string[] args)
{
var main = new Sample();
// Delegateに代入
CalculateMethod calc = new CalculateMethod(main.Add) + newCalculateMethod(main.Sub);
// 以下でもよい。
//CalculateMethod calc = new CalculateMethod(main.Add);
//calc += new CalculateMethod(main.Sub);
calc(10, 20);
Console.ReadLine();
}
}
結果は以下の通りです。
デリゲート(C# 2.0)
基本的なデリゲートの機能的には1.0と大差ないのですが、デリゲートの代入時にインスタンス生成(new デリゲート名)を省略できるようになりました。
これによって宣言にあった冗長な表現が簡略化されています。
// C# 1.0 // CalculateMethod calc = new CalculateMethod(main.Add); // calc += new CalculateMethod(main.Sub); CalculateMethod calc = main.Add; calc += main.Sub;
匿名メソッド・匿名関数(C# 2.0)
また、C# 2.0では匿名メソッド(anonymous method)・インラインメソッド(inline method)が追加されていました。
インラインメソッドとはdelegate構文を用いて「メソッド名を持たない処理」を記述する方法が提供されました。
// Delegate public delegate int CalculateMethod(int x, int y); static void Main(string[] args) { // 匿名デリゲート CalculateMethod calc = delegate(int x, int y) { return x + y; }; Console.WriteLine(calc(10, 20)); }
delegateを用いて非常に簡単は処理を記述することに利用されていましたが、今では後述するラムダ式があるため、利用する機会は減りました。
また、以前は前述のように匿名メソッド(anonymous method)や匿名関数(anonymous function)と記されていましたが、先日発売になったプログラミングC# 第7版ではこれらの機能のことを「インラインメソッド」と呼んでいました。
書籍によると根拠としては「C#の仕様書に【匿名関数はvoid型以外を返すインラインメソッドの別名として定義されています。】と記述がある」と記載されてありました。
つまり、「仕様書にはこう書いてるからこれが正しいんだ!」という主張でしたが、一応、念のためVSと一緒にインストールC#の仕様書を確認してみると、「インラインメソッド」ではなく「匿名関数」とがっつり書いてありました(^^;
やはり、きちんと原典は調べないとダメですね。 ちょっと余談でした(^^;
定義済みデリゲート型(共通デリゲート型)(C# 3.0/.NET Framework 3.5)
C# 2.0や.NET Framework 2.0で導入したジェネリック(総称性)の機能を用いて、.NET Framework 3.5では以下の3種類のデリゲートが定義されました。
戻り値のない定義済みデリゲートのAction
- 戻り値を持つ定義済みデリゲートのFunc
- 論理型のも戻り値を持つ定義済みデリゲートのPredicate
具体的にどのように定義されているかというと
public delegate void Action()
public delegate void Action<in T>(T arg)
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)
public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3)
public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
public delegate TResult Func<out TResult>()
public delegate TResult Func<in T, out TResult>(T arg)
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2)
public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3)
public delegate TResult Action<in T1, in T2, in T3, in T4, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
public delegate bool Predicate<T>(T arg)
というような感じで定義されています。
実は、.NET Framework 2.0で既にAction<T>(T arg)のみ定義されていましたが、その後3.0で、上記のように引数の数が4つあるジェネリックデリゲートまで定義されました。
最新の.NET Framework 4.5ではActionやFuncに指定される引数の数が最大16まであるデリゲートが定義されています。
そのため2.0までに行っていたように新しく「delegateキーワードを使ったデリゲート宣言」を行わなくてよくなり、多くのケースで定義済みのデリゲートを利用することで事足りるようになりました。
class Sample { public int Add(int x, int y) { return x + y; } public void Write(int s) { Console.WriteLine(s); } } class Program { static void Main(string[] args) { Sample s = new Sample(); // 戻り値のないメソッドをActionデリゲートで Action<int> write = s.Write; // 戻り値がある関数はFuncデリゲートで Func<int, int, int> calc1 = s.Add; // 匿名関数を使って宣言 Func<int, int, int> calc2 = delegate(int x, int y) { return x - y; }; write(calc1(10, 20)); write(calc2(10, 20)); } }
ラムダ式(C# 3.0)
ラムダ式は簡単に言ってしまうと「デリゲート型」および「式ツリー(ExpressionTree)」を作成するために使用できる匿名関数です。
主にLINQで用いることができるよう追加された仕様ですが、もちろんそれ以外の用途でも利用できます。
ラムダ式を作成するにはラムダ演算子( => ) を用いて左辺にパラメータ、右辺に式やステートメントブロックを置くことで作成できます。
// =>の左側がパラメータ、右側が式 Func<int, int> fuctorial = x => x*x; // パラメータが複数ある場合は()の中に指定する Func<int, int, int> calc2 = (x, y) => x + y; // 複数のステートメントになる場合はステートメントブロックを使って、 // メソッドと同様return文で戻り値を指定する。 Func<int, int, int> calc3 = (x, y) => { int result = x - y; return result; };
上記のようにラムダ式には式形式のものとステートメント形式のものがあります。
また、匿名関数といっても、前述した匿名関数とは見た目以外にもいくつかの違いがあります。
- ラムダ式の戻り値はデリゲート以外にも式ツリー(Expressionクラスを用いたツリー)も作成が可能。ただし、ラムダ式内でメソッド呼出などを行うと式ツリーの作成ができない。
- 匿名関数は、インラインで記述できるがメソッドなので、ステートメントブロックの中から定義されているメソッド内の変数にはアクセスできませんが、ラムダ式では「変数のキャプチャ」という機能により下記のサンプルのように上位スコープの変数のアクセスが行える。
Action<string> GetWriteAction(strings)
{
string text =string.Format(“[{0}]”);
// 上位スコープのtextをキャプチャ
return p =>Console.WriteLine(“{0}:{1}“, p, text);
}
ただし、上記のサンプルのように変数をキャプチャした状態で外部メソッドをデリゲートを引き継ぐと、通常メソッド終了時に変数の解放がされるところ、生存期間が長くなることがありますので注意が必要です。
まとめ
上記の流れを見ておわかりのように、C#1.0からはLINQ以降の流れの中で構文の書きやすさが著しくと向上しており、構文が簡潔になることでより直感的に利用することができるようになりました。
あらためてC#1.0の頃のデリゲートと今のスタンダードであるラムダ式を見比べてみると
C#1.0の定義
// デリゲートを定義 public delegate int CalculateMethod(int x, int y);
C#1.0での利用
CalculateMethod calc = new CalculateMethod(main.Add); Console.WriteLine(calc(10, 20));
現在のC#の定義
ほとんどのケースで必要なし(定義済みデリゲートを利用)
現在のC#での利用
Func<int, int, int> add = (x, y) => x + y; Func<int, int, int> sub = (x, y) => { int result = x - y; return result; };
このようにかなり簡潔に記述できるようになりました。