1. 介紹
在實際專案裡,幾乎每個工程師都有很大一部分時間在跟集合打交道:篩選、計數、搜尋、重新排序、放進去、拿出來 —— 基本上跟睡前站在冰箱前的行為差不多。特別常見的情況是要擷取、處理與聚合資料——不管是使用者清單、商品目錄、文字行還是其他任何陣列。
幾乎所有現代的 .NET 集合都支援一些函式風格的方法,例如 Where, Select, Find, Any, All 等等。它們的厲害之處在於通用性跟簡潔:你只要把一小段「邏輯」以 lambda 的形式傳進去,集合就活了起來,好像你換了一顆新引擎。
LINQ (Language Integrated Query) 不只是語法糖,它像是在 C# 裡的迷你語言,讓你寫資料查詢像寫 SQL 或 Excel 一樣。更棒的是:直接在程式碼裡,有自動補完、型別檢查跟除錯工具。
這些魔法其實都是靠 delegate 實現的——如果每次都為了篩選寫一個獨立的方法會很累。這時候 lambda 表達式就派上用場,當場寫小函式,讓笨重的程式碼變得優雅又表意。
2. Lambda 在標準集合方法中的應用
Lambda 在基於 delegate 的標準集合方法中特別好用,例如 Find, Exists, ForEach 等等。
範例:依條件搜尋
假設你有一個商品清單:
using System;
using System.Collections.Generic;
// 我們的商品類別
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); // 用 lambda!
Console.WriteLine(expensive?.Name); // => 咖啡
沒用 lambda 的話你得寫獨立方法或是老式的匿名方法。這樣一行就夠了,程式碼讀起來像英文: "Find the product where price > 90"。
範例:檢查商品是否存在
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (因為「茶」比75便宜)
範例:對每個元素做處理 (ForEach)
有時候你就是想對每個元素做點事:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} 歐元"));
關於比喻
簡單說:集合裡的 lambda 就像照片編輯器裡的「一鍵美化」。按下去,效果出來!
3. Lambda 與 LINQ:為集合帶來的魔法
LINQ 不只是方便,它還是進入函式式程式風格的好入口。大多數 LINQ 方法都接受 delegate,這讓 lambda 成為它們的最佳拍檔。
用 Where 篩選
假設我們還有之前的商品清單。現在挑出「便宜」的商品:
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // 茶, 牛奶
你得到了新的集合,完全不用自己寫迴圈。Where 接受一個回傳 true/false 的 predicate lambda,並把它套到每個元素上。
用 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); // 牛奶, 茶
看起來就像生產線一樣:每個方法都是一個處理階段。
問題:為什麼 Lambda 比一般方法更適合 LINQ?
第一,lambda 可以就地撰寫於需要的地方。第二,lambda 短又好讀。第三,這是現代 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 把這些包起來,而且把你的 predicate 包成 delegate,讓整體看起來更乾淨。
如果沒有 lambda,你可能得寫出像下面這種東西:
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; });
那些寫法都比較冗長又不爽。用 lambda 就優雅又現代。
6. Delegate 與常用類型: Func, Action, Predicate
不只是 LINQ 愛用 lambda,很多集合方法也接受專門的 delegate,例如:
- 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. 集合方法與 Lambda 的速查表
| 方法 | 作用 | Delegate 類型 | Lambda 範例 |
|---|---|---|---|
|
過濾元素 | |
|
|
投影 / 轉換 | |
|
|
依鍵排序 | |
|
|
符合條件的第一個元素 | |
|
|
是否存在符合條件的元素 | |
|
|
是否所有元素都符合條件 | |
|
|
符合條件的元素數量 | |
|
|
對每個元素做某事 | |
|
|
根據 Predicate 刪除所有 | |
|
8. 常見錯誤與注意事項
最常見的錯誤之一是忘了 LINQ 不會改變原本的集合,它會回傳一個新的序列。也就是說,執行完 var sorted = users.OrderBy(u => u.Age); 之後,原本的 users 仍然保持原來的順序!這常常讓人誤以為東西已經被排序,但其實沒有。
另一個細節:像 Where, Select 這類方法會回傳 IEnumerable<T>,這是一個延遲執行(lazy)的序列——真正的處理會在你真正開始列舉它時才發生(例如用 foreach、呼叫 ToList() 等)。所以如果你要把結果實體化,別忘了呼叫 ToList() 或 ToArray():
var sortedList = users.OrderBy(u => u.Age).ToList();
還有一點:如果 lambda 捕獲了外部作用域的變數(也就是閉包),那些變數會一直被保留在記憶體中,直到 lambda 的參考消失為止。通常不是大問題,但如果你在長生命週期的物件裡用 lambda 捕獲了一個很大的陣列,那個陣列就會跟著一起被保留。
最後一句建議:用語意清楚的參數與變數名稱——尤其當你有多層嵌套的 lambda 時,清楚的命名能大幅提高可讀性。
GO TO FULL VERSION