1. はじめに
面白いことに気づいた? ラムダ式自体は鍋のないレシピみたいなものだ。何をするかは書けるけど、コードの中で動かすには「コンテナ」が必要になる。C# にはそのための汎用デリゲートが用意されていて、それが Func、Action、Predicate だ。
必要な型をテンプレートとして使って、そこに自分のロジックを流し込めばOK。わざわざ自前のデリゲートを作って自転車を再発明する必要はほとんどない。
ちょっと歴史
関数をパラメータとして渡す必要が出てきた初期のころは、毎回デリゲート型を宣言しないといけなかった。面倒で長かった、こんな感じ:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
C# に Func、Action、Predicate が入ってからは、手動でデリゲートを宣言する必要は 90% のケースでなくなった。今はずっとシンプル:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — 戻り値のある関数
構文と用途
Func はジェネリックなデリゲートで、パラメータを0個から16個まで受け取り、値を返す。
Func<int, int, int> sum = (x, y) => x + y;
Func<型1, 型2, ..., 型N, TResult> — 最後の型が戻り値で、それ以前が引数になる。
例
1. 2つの数の合計
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. 数値を二乗する
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. パラメータ無し
Func<string> greet = () => "やあ、ラムダ!";
Console.WriteLine(greet());
視覚的な図
| シグネチャ | 例 | 説明 |
|---|---|---|
|
|
int を受け取り、int を返す |
|
|
2つの int を受け取り、int を返す |
|
|
何も受け取らず、文字列を返す |
アプリでの見た目
例えば小さなアプリ(仮の「ユーザー辞書」)で、数値のリストに色んな処理を適用したいとする。二乗したり、固定値と合計したり:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — 戻り値のない処理
Action は何かを実行するメソッド用の汎用デリゲート(画面に出力するなど)で、値を返さない。
パラメータを0個から16個まで取れるが、常にvoid を返す。
例
1. 画面に出力
Action<string> print = text => Console.WriteLine("データ: " + text);
print("やあ、世界!");
2. パラメータ無しのアクション
Action greet = () => Console.WriteLine("ようこそ!");
greet();
3. 複数パラメータのアクション
Action<int, int> showSum = (a, b) => Console.WriteLine($"合計: {a + b}");
showSum(2, 3); // 合計: 5
視覚的な図
| シグネチャ | 例 | 説明 |
|---|---|---|
|
|
パラメータ無し、戻り値無し |
|
|
1つのパラメータ |
|
|
複数パラメータ |
アプリでの例
ユーザー辞書に全員の名前を出力するメソッドを追加してみる:
List<string> names = new() { "アンナ", "ボリス", "ヴィカ" };
Action<string> printName = name => Console.WriteLine("ユーザー: " + name);
names.ForEach(printName);
// あるいは: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — yes / no?
1つのパラメータに対して true か false を返す関数が必要なときは Predicate<T> を使おう。これは1つのパラメータを受け取り bool を返すデリゲートだ。
Predicate は要するに「ブール判定が欲しい」用のラッパーだ。
例
1. 5 より大きいかをチェック
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. List<T>.Find と一緒に使う
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // Predicate<int> が使われる
Console.WriteLine(found); // 7
3. 全員が成人か?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll は Predicate<int> を受け取る
Func<T, bool> と何が違う?
実際にはほとんど互換性がある。Microsoft のドキュメントでも言われている通り、"Predicate<T> は特定の API のための Func<T, bool> に過ぎない"。ただし標準ライブラリのメソッドは時々あえて Predicate を要求することがある。
5. ラムダがどうやって Func、Action、Predicate に「はまる」か
ラムダを書くと、C# は「この形は要求されているデリゲートに合っているな」と解析して代入できるようにする。
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
どれもラムダだけど、内部ではシグネチャの違う3つの別々のデリゲートが動いている。
実際のコード例での適用
List<User> users = new() {
new User("アンナ", 24),
new User("ボリス", 17),
new User("ヴィカ", 31),
};
// 成人ユーザーだけを返す関数 (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("成人一覧: " + string.Join(", ", adults.Select(u => u.Name)));
全ユーザーの名前を Action<User> で出力したいなら:
users.ForEach(user => Console.WriteLine(user.Name));
名前を取得する (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
わかりやすい表
| デリゲート | シグネチャ | ラムダの例 | 使われる場所 |
|---|---|---|---|
|
T → U | |
Select、任意の変換 |
|
T → void | |
ForEach、処理メソッド |
|
T → bool | |
Find, Exists, フィルタ |
6. 内蔵アプリの例:ステップ・バイ・ステップ
小さな「ユーザー辞書」アプリを拡張してみよう。まずクラス User を用意する:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — ユーザーが成人かチェック
Func<User, bool> isAdult = user => user.Age >= 18;
LINQ で使う例:
var adults = users.Where(isAdult);
2. Predicate<User> — アクティブなユーザーを探す
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — ユーザーを非アクティブ化する
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — 短い説明を取得
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
これらのラムダは「データとしてのコード」で、メソッドに渡したり、変数に保持したり、組み合わせたりできる。
7. 気をつけるポイントとよくあるミス
1. ラムダはデリゲートのシグネチャに合っている必要がある。 シグネチャが合わなければコンパイルエラーになる。
Func<int, string> wrong = x => x * 2; // エラー: string が期待されましたが int が得られました
// 正しい例:
Func<int, string> right = x => (x * 2).ToString();
2. void と return を混同しないこと。 Action は値を返さない。例えば Action<int> a = x => x * x; は動かない。戻り値を返しているからだ。
3. Predicate<T> と Func<T, bool> はよく互換だが、常に同じではない。 コレクションのメソッドが特に Predicate<T> を要求することがあり、その場合は直接代入できない。
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // エラー
// ただし:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // コンストラクタ経由で変換できる
GO TO FULL VERSION