1. はじめに
デリゲート は関数の「署名」を表す型だ:どんなパラメータを取って、何を返すか。コードのためのクラブの入場パスみたいなものだね — 署名が合えば入れる、合わなければお断り。
例:
public delegate int Operation(int a, int b);
こんなデリゲートは int を2つ受け取って int を返す関数を表す。ラムダが来る前のコードはこんな感じ(新鮮だけどちょっと面倒):
Operation add = delegate (int x, int y)
{
return x + y;
};
ラムダのおかげでコードはもっと簡潔に書けるようになった:
Operation add = (x, y) => x + y;
ちなみに: C#のラムダ式は「匿名関数」を作るための単なる書き方で、適切な型のデリゲートのインスタンスに自動でコンパイルされる。だから両者の区別がほとんど気にならなくなるんだ。
組み込みデリゲート: Func, Action, Predicate
ケースごとにデリゲートをいちいち作らないように、C#には汎用の型がある:
- Func<T1, T2, ..., TResult> — パラメータ (T1, T2, ...) を受け取り、TResult を返す汎用デリゲート。
- Action<T1, T2, ...> — ほぼ同じだけど何も返さない(void)。
- Predicate<T> — 1つのパラメータを取り bool を返す(フィルタ系で大活躍)。
| デリゲート | 戻り値の型 | 構文の例 |
|---|---|---|
|
|
|
|
|
|
|
|
|
例:
Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
2. デリゲートという“封筒”に入ったラムダ
デリゲートを受け取るメソッドにラムダを渡す
デリゲートをパラメータに取る任意のメソッドは、署名が合えばラムダを「食べる」ことができるよ。
public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
int result = operation(a, b);
Console.WriteLine($"結果: {result}");
}
// ラムダを渡して呼び出す
CalculateAndShow(5, 7, (x, y) => x * y); // 結果: 35
Action の例
void ProcessString(string message, Action<string> processor)
{
processor(message);
}
// ラムダを使う
ProcessString("こんにちは、世界!", s => Console.WriteLine(s.ToUpper()));
Predicate の例
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
こんな時に、自前のデリゲートをいちいち作る理由が見当たらないってわかるはず — Microsoftの“車輪”を使えば楽だよ。
デリゲートとラムダがどう動くか
flowchart TD
A[ラムダ式] -->|生成する| B((デリゲート))
B -->|引数として渡される| C[メソッド]
B -->|保存される| D[プロパティ/フィールド/コレクション]
C -->|呼び出される| E[ラムダのコード]
3. 自分のメソッドにラムダをデリゲートとして渡す
自分のメソッドがデリゲートを使うなら、ラムダを受け取れるようにするとかなり柔軟になるよ。
例: ミニ電卓
// 演算用のメソッドを定義する
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
// いろんな演算を使う(ラムダ万歳)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);
Console.WriteLine(sum); // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max); // 4
別々のメソッドを三つ用意する代わりに、こんな風にロジックを差し替えられるのがエレガントだね。
4. LINQ でのラムダ + デリゲート(とかその他)
LINQ のメソッドは「小さな論理関数」を期待している、つまりデリゲートだ。ほとんどの標準LINQメソッドはこんな型を取る:
- Func<T, bool> はフィルタ(Where)用
- Func<T, TResult> は変換(Select)用
- Func<T, TKey> はソート(OrderBy)用
List<string> names = new List<string> { "アンナ", "オレグ", "ヴィクトル", "ヤナ" };
// 3文字より長い名前だけを取得する
var longNames = names.Where(name => name.Length > 3);
// 名前を大文字に変換する
var upperNames = names.Select(name => name.ToUpper());
LINQはまさに「ラムダのフルパワー」だ — デリゲートとして渡せるものは全部ラムダにできる。
5. 組み合わせ:ラムダデリゲートの保存と受け渡し
関数のリスト
ラムダは一時的な匿名メソッドだけじゃない。コレクションにラムダをためて動的に使うこともできる。
List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
(x, y) => x + y,
(x, y) => x - y,
(x, y) => x * y,
(x, y) => x / y
};
foreach (var op in operations)
{
Console.WriteLine(op(10, 2));
}
テスト、ハンドラ、プラグインなどでよく使うテクニックだよ。
デリゲートの辞書
plug-and-play なアーキテクチャが好きならこんな感じ:
var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
{ "plus", (x, y) => x + y },
{ "minus", (x, y) => x - y },
{ "pow", (x, y) => (int)Math.Pow(x, y) }
};
string command = "pow"; // ユーザー入力の模擬
if (mathFuncs.TryGetValue(command, out var operation))
{
Console.WriteLine(operation(2, 5)); // 32
}
else
{
Console.WriteLine("コマンドが見つかりません");
}
6. 便利な小ネタ
ラムダデリゲート:メソッドからデリゲートを返す
メソッドからデリゲート(つまりラムダ)を返すこともできる — 関数のファクトリみたいなもんだ。
Func<int, int> GetMultiplier(int factor)
{
// factorで掛けるラムダデリゲートを返す
return x => x * factor;
}
var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15
var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20
こうしてクロージャ(closure)が単なるおもちゃじゃなくて実用ツールになるんだ。
関数の合成: Combine, Delegate.Combine とマルチデリゲート
一つの「呼び出し」で複数の関数を実行したいことがある。GUIのイベント処理とかでボタンを押したら複数のハンドラが反応する、みたいな。
Action やデリゲートは複数の関数を“つなげる”ことができる:
Action<string> pipeline = s => Console.WriteLine("ステップ1: " + s);
pipeline += s => Console.WriteLine("ステップ2: " + s.ToUpper());
pipeline += s => Console.WriteLine("ステップ3: " + s.Length);
pipeline("テスト");
// 出力:
// ステップ1: テスト
// ステップ2: テスト
// ステップ3: 4
チェーンやイベント処理にはすごく便利。
重要: 値を返す関数をマルチキャストすると、戻り値は最後に呼ばれたハンドラの結果だけになる(初心者がよく驚くポイント)。Action は void なので全部実行されるよ。
ラムダデリゲートをコールバックのパラメータに使う
典型的なパターンは「終わったら呼んでね」って感じのコールバック。
void DownloadFile(string url, Action<string> onFinish)
{
// ファイルダウンロードの模擬...
System.Threading.Thread.Sleep(500); // (長時間処理の模擬、実際のGUIアプリでは使わないでください)
onFinish($"ダウンロード '{url}' が完了しました!");
}
DownloadFile("http://example.com", msg => Console.WriteLine(msg));
実際のアプリ(APIやDB、ファイル操作)では定番のパターンだね。
高階関数の例: Predicateによるフィルタ
ラムダとデリゲートを使えば、他の関数をパラメータに取る汎用関数が書ける。これが高階関数の基本。
List<int> Filter(List<int> source, Predicate<int> condition)
{
List<int> result = new List<int>();
foreach (var item in source)
if (condition(item)) result.Add(item);
return result;
}
// いろんなラムダを使って:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);
ラムダの“ファクトリ”を作ることもできる:
Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
x => x >= min && x <= max;
Predicate<int> inRange = GetRangeChecker(1, 10);
var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10
7. 組み合わせに関する典型的なミスと注意点
パラメータの署名が合わないとコンパイラが文句を言うよ(CS1660: "Cannot convert lambda expression")。
ラムダは思わぬ形で変数を「覚えてしまう」ことがある(キャプチャ / closure) — 変数のキャプチャに注意してね。
戻り値のあるマルチデリゲートは最後のハンドラの結果しか返さないので、これも驚きポイント。
Expression<Func<int,bool>> が必要な場面では statement-ラムダ は使えない、必ず expression-ラムダ が求められる、という細かい制約もあるよ。
GO TO FULL VERSION