1. はじめに
コーヒーマシンを想像してみて。普通はコーヒーを淹れて終わりなんだけど、たまに上司が「淹れ終わったら『できた!』って叫んでくれない?」って言うことがあるよね。マシン本体を書き換えたりはしない。代わりに、最後に実行する関数を渡すだけでいいんだ。
C#ではこの役割をデリゲートが担うよ — メソッドにコードの断片(メソッド、ラムダ、匿名メソッド)を渡して、適切なタイミングで呼び出せるようにする。ざっくり言えば、デリゲートは特定のシグネチャを持つメソッドへの参照を保持できる型だよ。
デリゲートの定義
C#ではデリゲートはキーワード delegate で定義する。例:
// int を受け取り bool を返すメソッドを参照するデリゲート
public delegate bool PredicateInt(int x);
これで PredicateInt 型の変数は、1つの int を受け取って bool を返す任意のメソッド(あるいはラムダ)を参照できるようになる。
デリゲートは何に使う?
- ロジックを引数として渡す(例えば、ソート、フィルタ、イベント処理など)
- イベントの購読(これは後で詳しくやる)
- コールバックの実装
- 呼び出し側が振る舞いの一部を決める柔軟なAPI設計
簡単なビジュアル図
| デリゲートの種類 | シグネチャ | 呼び出し例 |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. ラムダがデリゲートに変わる仕組み
構文
例えばラムダ式 x => x > 5 と書くと、実際にはデリゲートのオブジェクトを作っているんだ。ラムダは文脈(パラメータの型や戻り値の型)を知らないと独立して存在できない。だからC#のラムダ式は常に(暗黙的にも明示的にも)デリゲートに変換されるんだよ。
例1:デリゲートをメソッドに結びつける
// 明示的にデリゲートを定義する
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// MyPredicate 型の変数にラムダを代入
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
例2:標準デリゲートの利用
C# は汎用的な標準デリゲート群を持っている:Action、Func<>、Predicate<>。ラムダを書くほとんどの場所でこれらを使うことになるよ。
// Func
を使う Func
isPositive = number => number > 0; Console.WriteLine(isPositive(-5)); // false
3. 標準デリゲート: Func, Action, Predicate
Func<...>
何かを受け取って何かを返すメソッド用に使うよ。
シグネチャ:
— 最後の型が戻り値で、それ以前がパラメータの型。例えば:
Func<int, string> — int を受け取り string を返す
Func
intToString = number => "数: " + number; Console.WriteLine(intToString(7)); // "数: 7"
Action<...>
何かを実行するけど、結果を返す必要がない(void)ときに使う。
Action
printHello = name => Console.WriteLine("こんにちは、" + name + "!"); printHello("ヴァシリー"); // "こんにちは、ヴァシリー!"
Predicate<T>
本質的に Func<T, bool> の省略形。オブジェクトに対する論理チェック(true/false)のときに使う。
Predicate
isOdd = x => x % 2 != 0; Console.WriteLine(isOdd(3)); // true
ビジュアル早見表
| デリゲート | シグネチャ | 用途 |
|---|---|---|
|
|
変換、プロジェクション |
|
|
副作用、出力 |
|
|
フィルタリング、検索 |
ラムダにどのデリゲート型を選ぶ?
- 戻り値があるなら Func<...>
- 何も返さない(void)なら Action<...>
- 条件チェックが必要なら Predicate<T>
例:リストのフィルタリング
List
numbers = new List
{ 1, 2, 3, 4, 5, 6 }; // Predicate
を期待する List
evenNumbers = numbers.FindAll(x => x % 2 == 0); Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. 便利なポイント
ラムダ式とコレクションメソッド:内部で何が起きている?
例えばこんな呼び出しをすると:
var adults = users.Where(u => u.Age >= 18);
メソッド Where は引数として Func<T, bool> 型を期待している。つまり、ラムダ u => u.Age >= 18 はコンパイラによってその型のデリゲートオブジェクトに変換される、ということだよ。
ブロック図:仕組み
あなたのラムダ --> C#コンパイラ --> デリゲートのオブジェクト (Func
) (u => u.Age >= 18) [型は文脈から判明] (Where() 内で呼び出せる状態)
型について詳しく: 型推論
通常、デリゲートの型はコンパイラが文脈から自動で推論してくれる。例えば List<T>.Find は Predicate<T> を期待しているので、コンパイラはパラメータの型をメソッドのシグネチャから知っている。
List
words = new List
{ "one", "two", "three" }; var result = words.Find(word => word.Length == 5); // Find は Predicate
を期待する Console.WriteLine(result); // "three"
もし文脈が不明瞭なら、コンパイラを手伝ってやる必要があるよ:
// 明示的に型を指定する
Func
check = x => x > 10;
戻り値がデリゲート:関数ファクトリ
メソッドがデリゲートを返すこともできる — いわゆる「関数の工場」だね。動的な振る舞いを生成するのに便利だよ。
// デリゲート(ラムダ)を返す関数
Func
GetMultiplier(int factor) { return x => x * factor; } var times3 = GetMultiplier(3); Console.WriteLine(times3(5)); // 15
これは、ラムダ (x => x * factor) が外側の変数 factor を捕捉する(クロージャ)ため、Func<int, int> 型のオブジェクトとして返されても動くんだ。
5. デリゲートとラムダのエラーと誤解
シグネチャの不一致
パラメータや戻り値が合わないラムダをデリゲートに代入しようとすると、コンパイラは許してくれないよ。
Func
f = x => "文字列を返せません!"; // コンパイルエラー
デリゲートなしでラムダを使おうとして起きるエラー
型が分からないままラムダだけ書いて呼び出そうとしてもダメだよ:
// これは動かない - コンパイラは型を推論できない
// var myFunc = x => x * 2; // CS0815 エラー
// myFunc(10);
動かすには型を明示するか、文脈を与える必要がある:
Func
myFunc = x => x * 2; Console.WriteLine(myFunc(10)); // 20
Action、Func、Predicate の混同
たまに間違ったデリゲート型を選んでシグネチャ不一致になることがある。覚えておくシンプルなルール:結果があるなら Func、結果がないなら Action(void)、論理チェックなら Predicate<T>。
GO TO FULL VERSION