Linq 新功能 (5) DistinctBy、ExceptBy、IntersectBy 和 UnionBy

.NET 6 Linq 的新功能來到最終回合。

本集提要
  • 框架 : .NET 6
  • 功能 : DistinctBy、ExceptBy、IntersectBy 和.UnionBy
方法宣告

在 .NET6 中,這四個方法分別有兩個多載:

DistinctBy
  1. IEnumerable<TSource> DistinctBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)
  2. IEnumerable<TSource> DistinctBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
ExceptBy
  1. IEnumerable<TSource> ExceptBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector)
  2. IEnumerable<TSource> ExceptBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
IntersectBy
  1. IEnumerable<TSource> IntersectBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector);
  2. IEnumerable<TSource> IntersectBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource,TKey> keySelector, IEqualityComparer<TKey>? comparer)
UnionBy
  1. IEnumerable<TSource> UnionBy<TSource,TKey> (this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource,TKey> keySelector)
  2. 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);

 }
範例

本次的範例比較分散:

參考資料

Microsoft Learn .NET6 新的 LINQ API