1. はじめに
実プロジェクトでは、ほとんどのプログラマーがコレクションの扱いに多くの時間を費やします:フィルタリング、カウント、検索、並べ替え、入れたり取り出したり — 要するに、寝る前の冷蔵庫を相手にしているような感じです。特にユーザーのリスト、カタログ内の商品、テキスト行やその他の配列を抽出・処理・集計する機会が多いです。
ほとんどの最新の.NETコレクションは、Where、Select、Find、Any、Allなどの関数型メソッドをサポートしています。これらの力は汎用性と簡潔さにあり、ラムダ式という「ロジックの断片」を渡すだけで、コレクションがまるで新しいエンジンをかけたかのように動き出します。
LINQ(Language Integrated Query)は単なるシンタックスシュガーではなく、C#の中にある小さな言語で、まるでSQLやExcelを使うかのようにデータに対するクエリを書けます。しかもコード内で、補完や型、デバッガと一緒に使えるので便利です。
しかし、この魔法が動くのはデリゲートのおかげです — 配列のフィルタリングのためだけに毎回別のメソッドを書くのは疲れます。そこでラムダ式が現場でのミニ関数として活躍し、重たいコードをエレガントで表現力豊かにしてくれます。
2. コレクション標準メソッドでのラムダ式
ラムダ式は、Find、Exists、ForEachなどデリゲートベースの標準コレクションメソッドで特に輝きます。
例:条件による検索
例えば、商品リストがあるとします:
using System;
using System.Collections.Generic;
// 私たちのProductクラス
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
var products = new List<Product>
{
new Product { Name = "コーヒー", Price = 100 },
new Product { Name = "お茶", Price = 70 },
new Product { Name = "ミルク", Price = 80 }
};
// 最初の高額商品を見つける (>90)
Product expensive = products.Find(p => p.Price > 90); // ラムダを使ってる!
Console.WriteLine(expensive?.Name); // => コーヒー
ラムダがなければ別メソッドを書くか、旧スタイルの匿名関数を使う必要があります。ラムダなら一行で英語のように「価格が90より大きい商品を見つけて」と読めます。
例:商品の有無チェック
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (お茶は75より安いから)
例:全要素の処理 (ForEach)
時々、各要素に何かをしたいとき:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} ユーロ"));
類推
簡単に言えば、コレクション内のラムダ式は写真編集ソフトの「キレイにする」ボタンみたいなものです。押せば結果が出ます!
3. ラムダ式と LINQ:コレクションの魔法
LINQは便利なだけでなく、関数型スタイルの良い導入になります。多くのLINQメソッドはデリゲートを受け取るので、ラムダ式は自然な相棒です。
Whereでのフィルタリング
再びproductsリストを使って、今度は「安い」商品だけを選んでみます:
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // お茶, ミルク
新しいコレクションが得られ、手でループを書く必要はありません。Whereはラムダの述語(true/falseを返す関数)を受け取り、各要素に適用します。
OrderByによるソート
順序を付けたいなら:
var sorted = products.OrderBy(p => p.Price);
foreach (var p in sorted)
Console.WriteLine($"{p.Name}: {p.Price}");
// お茶: 70
// ミルク: 80
// コーヒー: 100
Selectでのマッピング(プロジェクション)
時にはオブジェクト全体ではなく、その一部だけが欲しいことがあります。例えば商品名のリスト:
var names = products.Select(p => p.Name);
foreach (var name in names)
Console.WriteLine(name); // コーヒー, お茶, ミルク
LINQのチェーン
LINQの良いところは、呼び出しをチェーンできる点です:
var namesOfCheap = products
.Where(p => p.Price < 90)
.OrderBy(p => p.Name)
.Select(p => p.Name.ToUpper());
foreach (var name in namesOfCheap)
Console.WriteLine(name); // ミルク, お茶 を大文字で出すと例:ミルク, お茶
生産ラインのチェーンのように、各メソッドが処理のステージになります。
質問:ラムダ式はなぜLINQの通常メソッドより優れているのか?
まず、ラムダは必要な場所で直接書けます。次に、ラムダは短く読みやすい。最後に、これは現代C#の標準的な書き方です — 使わない人は面接で苦労することが多いです。
4. 実践例
コースの間に、商品やユーザー、注文の小さなカタログを扱う学習アプリを書きました。ここにモダンなコレクション処理メソッドを追加してみましょう。
名前でユーザーを検索
public class User
{
public string Username { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User{ Username = "Alice", Age = 21 },
new User{ Username = "Bob", Age = 26 },
new User{ Username = "Charlie", Age = 32 }
};
// 名前でユーザーを検索
User found = users.FirstOrDefault(u => u.Username == "Bob");
Console.WriteLine(found?.Age); // 26
年齢でのフィルタリング
var adults = users.Where(u => u.Age >= 18);
foreach (var u in adults)
Console.WriteLine(u.Username); // Alice, Bob, Charlie
ユーザー数のカウント
int count = users.Count(u => u.Age > 25);
Console.WriteLine(count); // 2 (Bob と Charlie)
全ユーザーが成人かのチェック
bool allAdults = users.All(u => u.Age >= 18);
Console.WriteLine(allAdults); // True
未成年が一人でもいるか?
bool hasMinor = users.Any(u => u.Age < 18);
Console.WriteLine(hasMinor); // False
5. LINQ:内部でどう動くか
例えば、Where(u => u.Age > 20)と書くと、実際にはすべての要素をループして各要素の条件をチェックするループを作るのと大体同じです。違いは、LINQがそれを見えなくキレイに包んで、あなたの述語をデリゲートとして扱う点です。
もしラムダ式がなければ、こんな構造を書かないといけません:
public static bool AgeMoreThan20(User u) => u.Age > 20;
var adultUsers = users.Where(AgeMoreThan20);
あるいは昔ながらの匿名メソッド:
var adultUsers = users.Where(delegate(User u) { return u.Age > 20; });
どれも冗長で味気ないです。ラムダならスマートでモダンです。
6. デリゲートと標準型:Func、Action、Predicate
ラムダ式はLINQだけのものではありません。多くの標準コレクションメソッドは専用のデリゲートを受け取ります。例えば:
- Predicate<T> — Find, Exists, RemoveAllのため
- Func<T, TResult> — LINQメソッド、プロジェクション、計算のため
- Action<T> — 要素に対して何かをするが値を返さないメソッド(ForEachなど)
実例はこんな感じです:
// Predicate<T>
users.RemoveAll(u => u.Age < 30); // 30未満を全て削除
// Func<T, TResult>
var names = users.Select(u => u.Username);
// Action<T>
users.ForEach(u => Console.WriteLine(u.Username));
7. ラムダ式を使うコレクションメソッドのチートシート
| メソッド | 何をするか | デリゲートの型 | ラムダの例 |
|---|---|---|---|
|
要素をフィルタする | |
|
|
プロジェクト、変換する | |
|
|
キーによるソート | |
|
|
条件に合う最初の要素 | |
|
|
条件に合う要素が少なくとも一つあるか | |
|
|
全ての要素が条件を満たすか | |
|
|
条件に合う要素の数 | |
|
|
各要素に対して何かをする | |
|
|
述語に合うものを全て削除 | |
|
8. よくあるミスと注意点
よくあるミスの一つは、LINQが元のコレクションを変更せず、新しいシーケンスを返すことを忘れることです。つまり、var sorted = users.OrderBy(u => u.Age);と書いても、元のusersコレクションは元の順序のままです!これで混乱することがあります:見た目はソートされたように見えても、実際には違う場合があります。
別のポイント:Where、SelectなどはIEnumerable<T>型のオブジェクトを返します。これは「遅延」コレクションで、実際の処理はあなたが列挙(foreachやToList()など)を始めた時に起きます。結果をマテリアライズしたければ、ToList()やToArray()を呼ぶのを忘れないでください:
var sortedList = users.OrderBy(u => u.Age).ToList();
また、ラムダ式が外側のスコープの変数を参照する(クロージャ)場合、その変数はラムダへの参照が生きている限りメモリに残り続けます。あまり怖がる必要はありませんが、長寿命のオブジェクト内でラムダが「巨大な配列」などをキャプチャしていると、その配列がラムダと一緒にメモリに残ってしまいます。
さらに、パラメータや変数に意味のある名前を付けることを習慣にしてください — 特にネストしたラムダが複数ある場合、可読性がかなり向上します。
GO TO FULL VERSION