1. はじめに
これまでに見てきた通り、LINQ のメソッドにはラムダを渡して使ってきたよね。でも、どうやって C# のメソッドは「関数を渡せる」って認識しているの?
簡単に言えば、デリゲートは関数のインターフェイスのようなものだよ。シグネチャ(パラメータの型と返り値)を定義しておけば、そのシグネチャに合う任意のメソッド(ラムダも含む)を渡すことができるんだ。
文字列をパラメータとして渡してきたのと同じ感覚で、ロジックの塊を渡せる。ただし、パラメータの型がデリゲートになるだけだよ。
デリゲート:分かりやすい基本理論
C# ではデリゲートは「こんなシグネチャの関数だよ」という型を表すんだ。
// int を受け取って bool を返すデリゲート
public delegate bool IntPredicate(int x);
シグネチャに合う関数なら、どれでもその型の変数に代入できる:
bool IsEven(int n) => n % 2 == 0;
IntPredicate pred = IsEven;
もちろんラムダも使える:
IntPredicate pred = x => x % 2 == 0;
汎用デリゲート: Func, Action, Predicate
- Func<T1, ..., TResult> — パラメータ T1, ... を受け取り、TResult を返す関数。
- Action<T1, ...> — パラメータを受け取るが値を返さない関数(戻り値は void)。
- Predicate<T> — T を受け取って bool を返す関数。
2. ラムダをメソッドに渡す
学習用の小さなコンソールプロジェクトを考えてみよう。ユーザーのリストを扱うアプリで、これまでは LINQ を使ってフィルタしてきたけど、自分でラムダ条件を受け取るメソッドを書いてみるよ。
ラムダを受け取る自作メソッドを作る
// 例として User クラスを定義する(アプリに追加する)
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
// リストとデリゲート条件(ラムダ)を受け取るメソッド
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
var result = new List<User>();
foreach (var user in users)
{
if (predicate(user)) // ラムダを呼び出す!
result.Add(user);
}
return result;
}
これで任意のラムダを渡せるよ:
var users = new List<User>
{
new User { Name = "ヴァーシャ", IsActive = true },
new User { Name = "ペトヤ", IsActive = false },
new User { Name = "マーシャ", IsActive = true }
};
// アクティブなユーザーだけをフィルタする
var activeUsers = FilterUsers(users, user => user.IsActive);
foreach (var user in activeUsers)
Console.WriteLine(user.Name); // ヴァーシャ, マーシャ
これだけ。FilterUsers が Predicate<User> を期待しているので、対応するラムダ(小さな関数)を渡しただけだよ。
Func<T, TResult> を使うバリエーション
Predicate<T> は条件(bool を返す)向け。各ユーザーに対して何かを「計算」して結果を集めたいなら Func を使おう。
// 各要素に関数を適用して結果を集めるメソッド
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
var result = new List<TResult>();
foreach (var user in users)
{
result.Add(selector(user));
}
return result;
}
使い方:
var names = MapUsers(users, user => user.Name.ToUpper());
foreach (var name in names)
Console.WriteLine(name); // ヴァーシャ, ペトヤ, マーシャ
3. 便利なポイント
渡し方はいろいろ
ラムダだけでなく普通のメソッドも渡せるよ。シグネチャが一致していれば問題ない。
// 普通のメソッド
static bool NameHasS(User user) => user.Name.Contains("s");
// 普通のメソッドを渡す:
var usersWithS = FilterUsers(users, NameHasS);
// ラムダを渡す
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));
古いスタイルの匿名メソッドもある(でもやめたほうがいい):
var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });
今どきはラムダが主流だよ!
LINQ にラムダを渡すときに実際に何が起きているか
var result = users.Where(u => u.IsActive).ToList();
内部的に Where は Func<User, bool> を受け取っている。つまり、Func<...> を受け取るメソッドなら同じように使えるってことだよ。
パラメータが2つ欲しい場合
// フィルタ用に2つのラムダを受け取るメソッド
public static List<User> FilterUsersCustom(
List<User> users,
Func<User, bool> include,
Func<User, bool> exclude)
{
var result = new List<User>();
foreach (var user in users)
{
if (include(user) && !exclude(user))
result.Add(user);
}
return result;
}
使い方:
var customFiltered = FilterUsersCustom(
users,
u => u.Name.StartsWith("V"),
u => u.IsActive == false
);
// 名前が "V" で始まり、かつアクティブなユーザーだけを取る
シナリオ:フィルタファクトリ
Console.WriteLine("名前の最小文字数を入力してください:");
int minLength = int.Parse(Console.ReadLine());
Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);
// とてもインタラクティブで賑やかだね!
4. よくあるミスと注意点
コンパイラがラムダのパラメータ型を「推論」できない場合がある。特にオーバーロードが複雑だったり、メソッドが特定の返り値型や複数パラメータのデリゲートを要求しているときだ。そんなときはラムダの型を明示してあげればいい:
FilterUsers(users, (User u) => u.Name.Length > 3);
あるいは:
MapUsers(users, (User u) => u.Name.ToUpper());
エラー: ラムダのシグネチャが合わない
FilterUsers(users, user => Console.WriteLine(user.Name)); // エラー! bool が期待されているのに void が返された
期待されているのは bool を返す関数なのに、ラムダが void(つまり明示的に何も返していない)を返しているからだ。返り値の型には注意してね!
エラー: ラムダの使いすぎ
もしラムダが10行にもなるようなら、別メソッドに切り出したほうが読みやすいしデバッグもしやすいよ。
GO TO FULL VERSION