▼ ついでに後編(?)もあります。イベントとデリゲートの違いなどなど。
何度も分かった気分になって実は全然分かっていないデリゲートについてメモ。
幼児でもわかるデリゲート概要
とても雑にデリゲートとは?
メソッドの処理が書いてある "場所"を 覚えてくれて、そのメソッドを 呼び出せる 仕組み を持った クラスだよ。特別扱いされて クラスとは別の機構として 存在するよ。
①実行したいメソッドが メモリ上のどこにあるかを デリゲートに覚えさせておいて
②実行したくなった時に そのデリゲートを呼び出すと
③デリゲートが そのメソッドを 実行してくれるよ。
とても雑にデリゲートの何が便利?
メソッドを 変数のように扱えるから、あるメソッドを 別のメソッドに 引数で渡したりして 後から 実行してもらえるよ。(コールバック)
C#では、1つのデリゲートに 複数のメソッドを 紐づけられるから、デリゲートを1回呼び出すだけで 紐づけた複数のメソッドを ぜーんぶ 実行してくれちゃうよ。(マルチキャストデリゲート)
C言語の関数ポインタと比較で理解
- C#のデリゲートは、C言語の関数ポインタのような存在。
- C言語の関数ポインタは関数1つだけしか登録できない。(登録というかただの関数の先頭アドレスだもの。みつを)
- C#のデリゲートは、1つのデリゲートに複数のメソッドを登録できる。
- だからC#のデリゲートは、正確には関数ポインタの配列を持ってるやつって感じ。
C#のデリゲートの特徴
- デリゲートは System.Delegate クラスの派生クラス。(おまえクラスやないか…?!)
- デリゲートはメソッドの引数で渡せる。(C言語の関数ポインタでも可能だが)
- C#のデリゲートは、1つのデリゲートに複数のメソッドを登録/登録解除できる。(マルチキャスト機能)
C#のデリゲートの基本的な使い方
デリゲートを自作して使うまでの流れを体験して基本をおさえる。
STEP1 デリゲートの"型"を定義
1つのデリゲートには、定義したとおりの 引数の型・戻り値の型を持つメソッドしか登録できない。
定義すること
- デリゲート型の名前
- デリゲートに登録できるメソッドの引数の型
- デリゲートに登録できるメソッドの戻り値の型
delegate 戻り値の型 デリゲート型名(引数リスト);
delegate int CalcDelegate(int x, int y);
- やっていることはクラスや構造体の定義と同じ。あくまで"型"の定義。
- 定義のしかたは、メソッドの定義とほぼ同じ。違いはdelegateキーワードをつけるだけ。メソッドの代わりに呼び出すからメソッドのような存在でもある。
- デリゲートの正体はSystem.Delegeteクラスの派生クラスなので、普通はクラスの外に書くらしい。
- 内部クラスと同じようにクラス内に定義することも一応できる。
- メソッド内には定義できない。
STEP2 デリゲート変数を宣言
デリゲートはクラスと似ていて、型をそのまま使うのではない。
その型どおりにメモリ領域を確保し、そこにインスタンスを生成して使えるようになる。
あえて宣言のみを書いてみる。
型どおりにメモリ領域を確保しているだけ。
型 変数名;
CalcDelegate d;
STEP3 メソッドを登録(1つだけ)
例のために用意した Add, Subtract, Multiply, Divide の4種類のメソッドはコチラ。(単純な四則演算メソッド群だよ)
public static int Add(int x, int y)
{
Console.WriteLine("Add");
return x + y;
}
public static int Subtract(int x, int y)
{
Console.WriteLine("Subtract");
return x - y;
}
public static int Multiply(int x, int y)
{
Console.WriteLine("Multiply");
return x * y;
}
public static int Divide(int x, int y)
{
Console.WriteLine("Divide");
return x / y;
}
デリゲート変数の初期化も兼ねて1つ目のメソッドを "="演算子 で登録できる。
- メソッドをそのまま代入する方法 :
変数 = メソッド名;
- 明示的にインスタンス生成しながら代入する方法 :
変数 = new 型(メソッド名);
CalcDelegate d1 = Add; // 宣言+メソッドをそのまま代入
CalcDelegate d2 = new CalcDelegate(Add); // 宣言+明示的にインスタンス生成しながら代入
// CalcDelegate d3 = new CalcDelegate(); // 「引数入れろ」でコンパイルエラー
STEP4 デリゲート経由でメソッドを実行
変数名(引数リスト);
int return1 = d1(6, 3); // Add(6, 3) の結果 9 が返る
STEP5 メソッドを登録(複数)
-
C#のデリゲートには複数のメソッドを登録/登録解除できるマルチキャスト機能がある。
- メソッドを追加登録するために "+=" 演算子 を使える。
- 登録したメソッドを削除するために "-=" 演算子 を使える。
-
ローカル変数では、明示的な初期化が必要なので
- 初期化前に "+=" 演算子 は使えない。(コンパイルエラー)
- 初期化と同時に1つ目のメソッドを "=" 演算子 で登録するか、
- 既定値
Default
を代入するか。
-
メンバ変数では、明示的な初期化を怠っても既定値で埋めてくれるので
- 最初から "+=" 演算子 を使うことができる。
- (→自分が今登録したメソッドが何個目か?を気にする必要はない。)
// CalcDelegate d1 += Add; // 初期化前に"+="を使うとコンパイルエラー
CalcDelegate d1 = Add; // 初期化を兼ねて1つ目のメソッドを"="で登録
CalcDelegate d2 = Default; // 初期化するためだけに既定値を入れてみた
d2 += Add; // 1つ目のメソッド登録にも"+="が使えるようになった
d1 += Subtract; // メソッドを追加登録する
d1 += Multiply; // メソッドを追加登録する
d1 += Divide; // メソッドを追加登録する
int return2 = d1(6, 3); // 最後に登録されているDivideの戻り値が返る
d1 -= Divide; // 登録したメソッドを削除する
int return3 = d1(6, 3); // 最後に登録されているMultiplyの戻り値が返る
- 実行順は登録順になる。
- 戻り値は最後に実行されたメソッドのものになる。
定義済デリゲート
.NETの標準ライブラリに存在するデリゲート型のこと。
実践では定義済デリゲートで事足りる場合がほとんどで、デリゲートを自分で定義しないといけない時ってそう多くない。
以下、定義済デリゲート界で超有名な2つ。
1 Funcデリゲート
戻り値ありのメソッドを登録できるデリゲート。
引数は0個~16個まで対応できる。[公式]
引数の型・戻り値の型は何でもOK。
public delegate TResult Func<in T1,in T2,in T3,out TResult>(T1 arg1, T2 arg2, T3 arg3);
2 Actionデリゲート
戻り値voidのメソッドを登録できるデリゲート。
引数は0個~16個まで対応できる。[公式]
引数の型は何でもOK。
public delegate void Action<in T1,in T2,in T3>(T1 arg1, T2 arg2, T3 arg3);
FuncとActionの2つだけで十分に万能ですね!
できないことは…引数17個以上のメソッドを登録する、とか?
…ちなみに定義済デリゲートのMSのドキュメント、日本語で検索すると出てきにくいのだが
「xxxデリゲート」ではなく「xxx代理人」で検索するとトップに出てくる (苦笑)
え、これ公式用語なの?()
【おまけ1】実務1年目でお世話になったデリゲートの例
自分が初めてデリゲートを学んだ時、「それで、いつ使うねん???」という印象が強かったので、実務で登場した例もメモっておく。
定義済デリゲート、特にLinqというライブラリを使う時にFunc型デリゲートには頻繁にお世話になっている。
例として、LinqのWhereというメソッドを。
配列やリストから条件に合致した要素だけを取得するためのメソッド。
これは拡張メソッドなので、実質の引数は Func<TSource,Boolean>
の1つだけという感覚。
Where<TSource>(this IEnumerable<TSource>, Func<TSource,Boolean>)
実務でのFunc使用例。ソースはMSからパチってきた。
fruitsリストから、文字長6未満の要素だけを抽出する(ためのクエリを作っている)例。
List<string> fruits =
new List<string> { "apple", "passionfruit", "banana", "mango",
"orange", "blueberry", "grape", "strawberry" };
IEnumerable<string> query = fruits.Where(fruit => fruit.Length < 6);
fruit => fruit.Length < 6
これがFuncデリゲート型の"引数"です!
え、なんか引数に式渡してるの何って?
そうです、もちろんさっきこの記事でやったように、
①メソッドを予め用意する
②デリゲート変数に代入する
っていう手順を踏んだあとに、その変数を引数に渡すっていう方法も使えるけど…。
実際の開発では、こういう1箇所でしか使わない処理のためにわざわざメソッドを用意するのは無駄だったり逆にソースを追いにくくなったりすることがある。そういうときは代わりにラムダ式を直接引数で渡す。
【おまけ2】デリゲートと一緒に勉強したほうがいいもの
-
ラムダ式 ★★★
デリゲートを使う時によく使われるコードの書き方だよ。見た目がいかついよ。
使い捨てのメソッド(処理)を書くよ。
特に短い処理には最高に便利だよ。
慣れるまでは見るたび嫌悪感がすごいけど頑張ってね。 -
匿名メソッド ★☆☆
ラムダ式の前段だよ。
ラムダ式と同じく、使い捨てのメソッド(処理)を書くよ。後発のラムダ式のほうが簡潔に書けることが多くて、使われることが減ってるらしいけど、ラムダ式の解説では匿名メソッドと比較されがちだから、軽く知っておいたほうがラムダ式を理解しやすいよ。 -
ジェネリクス ★★☆
Action<T>とかFunc<TResult, T>とかについてる "<T>"←こいつだよ。
デリゲート関係ないとこでも見かけがちだよ。
ActionやFuncの引数・戻り値の型が何でもOKなように、柔軟なクラスを作れるなどなど、オブジェクト志向の真骨頂みを味わえるよ。 -
コールバック ★★★
「後でこのメソッドを呼んでね」と予め約束しておき、その後あるタイミングで約束通りにそのメソッドを実行することだよ。まさにC#のデリゲートがやってる動きだよ。
実行されるメソッドのことは言語を問わず「コールバック関数」とか単に「コールバック」とも呼ばれるよ。
コールバックという名の通り、「この留守電に気づいたら折り返し連絡ください」って感じだよ。
【おまけ3】デリゲートの次に勉強できること
-
イベント駆動型プログラム ★★★
代表格はGUIアプリだよ。
最初から最後まで順番に実行して終わりの単純なアプリとは違って、ボタンを押したときに決まった処理をしてくれるみたいなタイプの動きするプログラムだよ。
※人間からの命令だけじゃなくて、自分以外のアプリやOSからの命令を受け付けるようなものもあるから、GUIアプリみたいに画面を持ってないアプリでもイベント駆動型プログラムは全然あるよ。 -
イベント処理(イベント、イベントハンドラなど) ★★★
- C#でイベント駆動型プログラムを実現しているのはデリゲート。「ある出来事が起こった時にこのメソッドを呼んでね」という約束とそのコールバックという、イベント駆動型プログラムに欠かせない任務を担っている。
- イベント駆動型プログラムでは、「ユーザーがボタンを押す」などの、処理が走るきっかけになる出来事のことを概念的に「イベント」という。
- イベント駆動型プログラムでは、あるイベントが発生したときに実行される処理のことを概念的に「イベントハンドラ」という。
- C#では、イベント駆動型プログラムを安全に開発するためにデリゲートを最適な形にして使う仕組みとして「イベント」というメンバ機構が用意されている。メンバってことは、フィールド、プロパティ、メソッド、コンストラクタの仲間!
- C#では、あるイベントが発生したときに実行されるメソッドと、そのメソッドを登録してあるデリゲートを「イベントハンドラ」と呼ぶ。
▼デリゲートが分かったら次はイベント処理に進もう。