開發者常感困惑:LINQ 提供 Distinct 卻無 DistinctBy (依鍵去重);提供 Select 卻無 ForEach (副作用迭代)。本文從語言設計哲學、效能、組合性角度解析微軟的決策邏輯,並給出正確的替代方案。
Distinct 僅支援完整物件比較 (需實作 IEquatable<T> 或傳入 IEqualityComparer<T>),無法直接「依屬性去重」。但透過現有運算子組合即可達成:
var persons = new[] {
new Person{Age=10,Name="a"}, new Person{Age=10,Name="b"},
new Person{Age=10,Name="c"}, new Person{Age=20,Name="d"}
};
foreach (var p in persons.GroupBy(o => o.Age).Select(g => g.First())) {
Console.WriteLine(p); // Age=10 (a), Age=20 (d)
}
原理:GroupBy(keySelector) 分組 → Select(g => g.First()) 取每組第一筆。此組合即為 DistinctBy 語義。
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
return source.GroupBy(keySelector).Select(g => g.First());
}
微軟不在 IEnumerable<T> 提供 ForEach,主要基於三大理由:
ForEach 回傳 void,無法接續後續運算子 (如 .Where(...).ForEach(...).Select(...)),違反 LINQ 流式設計。ForEach 強制立即迭代執行副作用,喪失組合優化機會。ToList()) 以 foreach 陳述式明確處理,語意更清晰。public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) {
foreach (var t in source) action(t); // 破壞鏈式、破壞延遲
}
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action) {
foreach (var t in source) action(t);
return source; // 仍會立即執行 action,喪失延遲特性
}
GroupBy(...).Select(g => g.First()) 或引用 MoreLINQ 套件 (DistinctBy 經過充分測試)。foreach (var item in query) { ... }。若需在管線中執行副作用並保留延遲,可考慮 Do (Reactive Extensions) 或自訂 IEnumerable<T> 迭代器,但通常 ToList().ForEach 或 foreach 已足夠。標準 LINQ 缺少 DistinctBy/ForEach 非疏漏,而是刻意設計:前者可由現有運算子零成本組合,後者為避免破壞「延遲執行、鏈式組合、純查詢」三大核心價值。理解設計意圖,才能寫出地道、高效、可維護的 LINQ 代碼。