CodeGym /課程 /C# SELF /集合裡的 Lambda 表達式和 LINQ

集合裡的 Lambda 表達式和 LINQ

C# SELF
等級 49 , 課堂 2
開放

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 範例
Where
過濾元素
Func<T, bool>
p => p.Price > 100
Select
投影 / 轉換
Func<T, U>
p => p.Name
OrderBy
依鍵排序
Func<T, K>
u => u.Age
FirstOrDefault
符合條件的第一個元素
Func<T, bool>
u => u.Username == "Bob"
Any
是否存在符合條件的元素
Func<T, bool>
u => u.Age < 18
All
是否所有元素都符合條件
Func<T, bool>
u => u.Age >= 18
Count
符合條件的元素數量
Func<T, bool>
p => p.Price > 50
ForEach
對每個元素做某事
Action<T>
u => Console.WriteLine(u.Name)
RemoveAll
根據 Predicate 刪除所有
Predicate<T>
u => u.Age < 18

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 時,清楚的命名能大幅提高可讀性。

2
任務
C# SELF, 等級 49, 課堂 2
上鎖
使用 Lambda 運算式過濾清單
使用 Lambda 運算式過濾清單
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION