1. 前言
過濾就是從集合裡挑出符合某個條件的元素。想像一下:你的資料庫裡有 10 000 個商品,結果經理只要那「區區」12 個,還要符合某個新奇的條件。每次都手動寫 foreach 跟一堆 if,超麻煩,程式碼也會變得很難讀。LINQ 幫你搞定這些雜事。
在實際應用裡,過濾是最常見的資料操作:只顯示使用者感興趣的紀錄、把不想要的元素從集合裡「清掉」、做報表、搜尋或寄 e-mail 的時候都會用到。面試時 LINQ 過濾的題目也超常見。搞懂這一塊,對新手 .NET 工程師來說超重要。
2. 認識 Where 方法
意義跟運作原理
Where 方法是 LINQ 的擴充方法,它會接收一個 function(或 lambda 表達式),這個 function 決定過濾的條件。它會回傳一個新的序列(不會改到原本的!),裡面只會有條件回傳 true 的元素。
主要簽名:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
不用怕 generics 跟那些看起來很兇的詞。現在你只要抓住重點:Where 會吃進你給的集合跟條件。實際上超簡單:
- source — 你要過濾的集合(像是 List<Product>)
- predicate — function/條件(像 p => p.Price > 100)
簡單小題目熱身一下
假設我們有一個商品清單,只想挑出價格大於 100 的。以前都要寫 foreach 迴圈,手動把符合的加進新 list。LINQ 讓你一行就搞定。
3. 過濾
我們的 Product 類別跟起始集合
上一個單元我們大概寫過這樣的商品類別:
class Product
{
public string Name { get; set; }
public double Price { get; set; }
// 為了讓輸出好看一點:
public override string ToString()
{
return $"{Name} (價格: {Price})";
}
}
假設我們有一個商品清單:
var products = new List<Product>
{
new Product { Name = "麵包", Price = 30 },
new Product { Name = "牛奶", Price = 87 },
new Product { Name = "起司", Price = 250 },
new Product { Name = "巧克力", Price = 130 }
};
用 Where 過濾價格超過 100 的商品
var expensiveProducts = products.Where(p => p.Price > 100);
foreach (var product in expensiveProducts)
{
Console.WriteLine(product);
}
底層發生什麼事:
- Where 會把 products 裡的每個元素都跑一次,
- 每個都丟進 function(這裡是 p => p.Price > 100),
- 如果 function 回傳 true,這個商品就會進到新集合。
螢幕上的結果:
起司 (價格: 250)
巧克力 (價格: 130)
注意:原本的 products 清單完全沒被動到!LINQ 不會亂動你的資料。
4. 延遲過濾(Deferred Execution)
這裡有個很酷也很重要的點:Where 的結果只會在你真的去用它的時候才計算。像是你開始跑 foreach 迴圈時,LINQ 才會「走」原本的集合,邊走邊過濾。如果你根本沒用結果,啥事都不會發生。這就叫 延遲執行(deferred execution)。
這樣有一堆好處:
- 你可以串超長的方法鏈(像是又過濾又排序),只有第一次真的用到結果時才會執行。
- 省記憶體:不會產生一堆中間集合。
- 需要的時候可以中斷執行,或找到夠多元素就停。
視覺化流程圖
原始集合 —► Where (條件) —► 只走需要的元素
[ x ] [ x ] [ + ] [ + ] —► p.Price > 100 —► [ + ] [ + ]
(x — 不符合條件的元素,+ — 符合的)
5. 語法變化
經典擴充方法語法(Method Syntax)
我們現在用的就是這種寫法:
var result = products.Where(p => p.Price > 100);
另一種:Query Syntax
LINQ 也支援很像 SQL 查詢的語法:
var result = from p in products
where p.Price > 100
select p;
結果一模一樣!
選哪種語法看你爽,不過 method syntax(點方法)在專案裡比較常見,尤其是要串很多方法的時候。
6. 複雜條件過濾
多個條件
你可以用邏輯運算子(&&、||、!):
// 找所有貴的商品,除了「巧克力」
var filtered = products.Where(
p => p.Price > 100 && p.Name != "巧克力"
);
字串過濾(子字串、大小寫、Contains)
// 名稱裡有「ㄌ」的商品
var lProducts = products.Where(p => p.Name.Contains("ㄌ"));
注意:Contains 會區分大小寫!如果你想「不分大小寫」,可以這樣寫:
var lProducts = products.Where(p => p.Name
.ToLower().Contains("ㄌ")); // 這樣「牛奶」跟「巧克力」都會被選到
多集合過濾
假設你有兩個 list — 商品跟付款。你可以過濾那些有出現在付款清單裡的商品——不過這時候用 Any 跟 Join 會比較好,這部分之後會講。先記得:LINQ 也能搞很複雜的過濾!
7. 實用小細節
巢狀過濾跟方法鏈
你可以連續呼叫好幾次 Where,把過濾分好幾步:
var filtered = products
.Where(p => p.Price > 100)
.Where(p => p.Name.StartsWith("巧"));
不過最好還是把條件寫在同一個 Where,用邏輯運算子連起來——這樣效率比較好,程式碼也比較乾淨。
內建比較器跟自訂 function
有時候內建的運算子不夠用。像是你想過濾字串時不分大小寫又要考慮文化設定。這時候可以用比較方法:
var rusProducts = products.Where(
p => p.Name.StartsWith("巧", StringComparison.OrdinalIgnoreCase)
);
8. 過濾跟使用者輸入
來試試簡單的互動過濾吧!比如問使用者最小價格,然後顯示所有符合的商品。
Console.Write("商品的最低價格? ");
var minPriceStr = Console.ReadLine();
if (double.TryParse(minPriceStr, out double minPrice))
{
var filtered = products.Where(p => p.Price >= minPrice);
foreach (var product in filtered)
{
Console.WriteLine(product);
}
}
else
{
Console.WriteLine("錯誤:輸入的不是數字。");
}
現在我們這個「迷你小程式」已經有點像網路商店了!
9. 用 Where 常見的錯誤跟陷阱
寫程式跟人生一樣,總有一些坑。這些要小心。
忘記呼叫 ToList() 或 ToArray()
Where 的結果 不是 list 也不是 array!它是一個 IEnumerable<Product> 物件。大部分時候你可以直接在 foreach 裡用,但如果你真的需要集合(比如要用 index 存取),記得要呼叫 .ToList() 或 .ToArray():
var filteredList = products.Where(p => p.Price > 100).ToList();
如果忽略這點,很容易遇到像 "在列舉時集合被修改" 這種錯誤——尤其是你在過濾時又去改原本的集合。
改動原始集合
因為 LINQ 過濾是延遲執行,如果你在跑迴圈時刪掉原本 list 裡的元素,可能會噴例外。這在多執行緒或你在過濾時直接改集合時特別容易發生。
Null 值跟條件 function
如果集合裡有 null,你在條件 function 裡沒檢查就直接用物件的欄位,會噴 NullReferenceException。像這樣:
var filtered = products.Where(p => p.Name.StartsWith("阿"));
如果 products 裡有某個元素是 null,程式就會爆掉。
建議:如果有可能有 null,條件 function 裡一定要先檢查:
var filtered = products.Where(p => p != null && p.Name.StartsWith("阿"));
GO TO FULL VERSION