.NET 6 Linq 的新功能來到最終回合。
本集提要
- 框架 : .NET 6
- 功能 : DistinctBy、ExceptBy、IntersectBy 和.UnionBy
方法宣告
在 .NET6 中,這四個方法分別有兩個多載:
DistinctBy
- IEnumerable<TSource> DistinctBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)
- IEnumerable<TSource> DistinctBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
ExceptBy
- IEnumerable<TSource> ExceptBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector)
- IEnumerable<TSource> ExceptBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
IntersectBy
- IEnumerable<TSource> IntersectBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector);
- IEnumerable<TSource> IntersectBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
UnionBy
- IEnumerable<TSource> UnionBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource,TKey> keySelector)
- IEnumerable<TSource> UnionBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer);
Distinct vs DistinctBy
我們先瞧瞧 Distinct 替代關係,就 DistinctBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>) 這個多載而言,很明顯在替代 Distinct 中帶有 IEqualityComparer<TSource> 的多載,舉個簡單的例子,如果我有以下字串陣列:
string[] colors = { "red", "green", "blue", "green", "yellow", "blue" };
如果想要取得不重複的字串序列,你應該會這麼用:
var distinctColors = colors.Distinct();
.NET 6 以後你也可以這麼用:
var distinctColors = colors.DistinctBy(c => c);
明顯地脫褲子放屁,這原因在於 Distinct<TSource>(IEnumerable<TSource>) 會使用元素的相等比較,所以用字串自己的相等比較就可以決定『獨特的字串』(註1),所以在此等情境下,直接用原來的 Distinct 就可以了。
另外一個例子就會凸顯出 DistinctBy 是比較容易撰寫的,假設我們要獨一化的條件是『字串長度』,在這個需求下如果用 Distinct 就得先實作一個 EqualityComparer:
public class StringLengthEqualityComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
if (x == y) { return true; }
if (x == null || y == null) { return false; }
return x.Length == y.Length;
}
public int GetHashCode(string obj)
{
if (obj == null) { return -1; }
return obj.Length;
}
}
然後這麼使用它:
var distinctColorsByLength = colors.Distinct(new StringLengthEqualityComparer());
但如果用 DistinctBy 可以直接寫,無須另外新增類別:
distinctColorsByLength = colors.DistinctBy(c => c.Length);
註1: 我說的籠統了一些,事實上原始碼內部要取得實作的 EqualityComparer 另外牽扯到要不要區分大小寫的問題,還挺囉嗦一把的。
Union vs UnionBy
UnionBy 的替代形式近似於 DistinctBy,假設有以下兩個陣列,Union (聯集) 的條件是字串的長度:
string[] colors = { "red", "green", "blue", "green", "yellow", "pink" };
string[] insects = { "ant", "bee", "beetle", "fly", "butterfly", "grasshopper" };
用 Union 會這樣寫,和前面的 Distinct 一樣,必須先建立一個 EqualityComparer:
public class StringLengthEqualityComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
if (x == y) { return true; }
if (x == null || y == null) { return false; }
return x.Length == y.Length;
}
public int GetHashCode(string obj)
{
if (obj == null) { return -1; }
return obj.Length;
}
}
var result = colors.Union(insects, new StringLengthEqualityComparer());
這個情況使用 UnionBy 就簡單多了:
var result = colors.UnionBy(insects, c => c.Length);
ExceptBy 與 IntersectBy
這兩個會一起談是有原因的,因為 ExceptBy 和 IntersectBy 的使用情境和 UnionBy 是不一樣的。
UnionBy 還是兩個同元素的集合取得聯集,瞧瞧 UnionBy 的前兩個參數都是 IEnumerable<TSource>,但 ExceptBy 和 IntersectBy 第一個參數是 IEnumerable<TSource> ,第二個卻是 IEnumerable<TKey>,而原來的 Union/Except/Intersect 方法前兩個參數都是 IEnumerable<TSource>。
IEnumerable<TSource> 代表的是來源序列,這沒問題;至於第二個 IEnumerable<TKey> 則是作為 Except 或 Intersect 條件的鍵值,這樣說有點模糊,我們來舉個例子。
假設有一個 Student 陣列裡有七個元素:
static Student[] CreateStudents()
{
new Student { Id = 1, Name = "Bill" },
new Student { Id = 2, Name = "Steve" },
new Student { Id = 3, Name = "Ram" },
new Student { Id = 4, Name = "Alex" },
new Student { Id = 5, Name = "Mary" },
new Student { Id = 6, Name = "David" },
new Student { Id = 7, Name = "Steve" }
};
我們想要以 Id 值做 Except 或 Intersect,所以來一個 int 陣列:
int[] ids = { 1, 4, 6 };
於是我們就可以這樣搞:
static void Main(string[] args)
{
int[] ids = { 1, 4, 6 };
Student[] students = CreateStudents();
var result = students.ExceptBy(ids, s => s.Id);
Display(result);
Console.WriteLine("--------------");
result = students.IntersectBy(ids, s => s.Id);
Display(result);
}
範例
本次的範例比較分散: