實現一個基於相等性比較的 GroupBy

實現一個基於相等性比較的 GroupBy

Intro

在我們的系統裡有些資料可能會有問題,資料來源頭不在我們這裡,資料不好修復,在做

GroupBy

的時候就會很痛苦,預設的 group by 會依賴於

HashCode

,而某些場景下

HashCode

可能並不太大做統一,所以擴充套件了一個不依賴

HashCode

,只需要考慮相等性比較的一個

GroupBy

Sample

我們有下面這樣的一些資料

var students = new StudentResult[]{ new() { StudentName = “Ming”, CourseName = “Chinese”, Score = 80, }, new() { StudentId = 1, StudentName = “Ming”, CourseName = “English”, Score = 60, }, new() { StudentId = 2, StudentName = “Mike”, CourseName = “English”, Score = 70, }, new() { StudentId = 1, CourseName = “Math”, Score = 100, }, new() { StudentName = “Mike”, CourseName = “Chinese”, Score = 60, },};

這些資料是一些學生成績,但是學生的資訊不全,學生資訊可能有 Id,可能有 Name,假設每個學生的 Id 和 Name 都是唯一的,不會重複,將上面的資訊按學生分組並獲取每個學生的總分數,你會怎麼實現呢?

Implement

預設的實現依賴於

HashCode

,實現原始碼可以參考文末連結,而多個欄位的

HashCode

比較難以統一,所以就想著自己擴充套件

GroupBy

,實現程式碼如下:

GroupBy

的返回值是

IEnumerable>

,預設的

Grouping

Add

方法是

internal

我們先自定義一個簡單

IGrouping

,實現程式碼如下:

private sealed class Grouping : IGrouping{ private readonly List _items = new(); public Grouping(TKey key) => Key = key ?? throw new ArgumentNullException(nameof(key)); public TKey Key { get; } public void Add(T t) => _items。Add(t); public int Count => _items。Count; public IEnumerator GetEnumerator() { return _items。GetEnumerator(); } IEnumerator IEnumerable。GetEnumerator() { return GetEnumerator(); }}

接著來實現我們的按相等性比較的

GroupBy

,實現如下:

public static IEnumerable> GroupByEquality(this IEnumerable source, Func keySelector, Func comparer){ var groups = new List>(); foreach (var item in source) { var key = keySelector(item); var group = groups。FirstOrDefault(x => comparer(x。Key, key)); if (group is null) { group = new Grouping(key); group。List。Add(item); groups。Add(group); } else { keyAction?。Invoke(group。Key, item); group。List。Add(item); } } return groups;}

我們來測試一下我們的

GroupBy

,測試程式碼:

var groups = students。GroupByEquality(x => new Student() { Id = x。StudentId, Name = x。StudentName }, (s1, s2) => s1。Id == s2。Id || s1。Name == s2。Name, (k, x) => { if (k。Id <= 0 && x。StudentId > 0) { k。Id = x。StudentId; } if (k。Name。IsNullOrEmpty() && x。StudentName。IsNotNullOrEmpty()) { k。Name = x。StudentName; } });foreach (var group in groups){ Console。WriteLine(“——————————————————-”); Console。WriteLine($“{group。Key。Id} {group。Key。Name}, Total score: {group。Sum(x => x。Score)}”); foreach (var result in group) { Console。WriteLine($“{result。StudentId} {result。StudentName}\n{result。CourseName} {result。Score}”); }}

輸出結果如下:

可以看到前面的資料分成了兩組,但是可以看到的資料裡仍然是資訊不全的,我們可以稍微改進一下上面的方法,修改後如下:

public static IEnumerable> GroupByEquality(this IEnumerable source, Func keySelector, Func comparer, Action? keyAction = null, Action? itemAction = null){ var groups = new List>(); foreach (var item in source) { var key = keySelector(item); var group = groups。FirstOrDefault(x => comparer(x。Key, key)); if (group is null) { group = new Grouping(key) { item }; groups。Add(group); } else { keyAction?。Invoke(group。Key, item); group。Add(item); } } if (itemAction != null) { foreach (var group in groups。Where(g => g。Count > 1)) { foreach (var item in group) itemAction。Invoke(item, group。Key); } } return groups;}

增加了一個

itemAction

,這裡加了一個 group count 大於 1 的條件,因為只有一個元素的時候,key 一定是來自這個元素不需要更新,所以加了一個條件,再來修改一下我們呼叫的示例:

var groups = students。GroupByEquality(x => new Student() { Id = x。StudentId, Name = x。StudentName }, (s1, s2) => s1。Id == s2。Id || s1。Name == s2。Name, (k, x) => { if (k。Id <= 0 && x。StudentId > 0) { k。Id = x。StudentId; } if (k。Name。IsNullOrEmpty() && x。StudentName。IsNotNullOrEmpty()) { k。Name = x。StudentName; } }, (x, k) => { if (k。Id > 0 && x。StudentId <= 0) { x。StudentId = k。Id; } if (k。Name。IsNotNullOrEmpty() && x。StudentName。IsNullOrEmpty()) { x。StudentName = k。Name; } });foreach (var group in groups){ Console。WriteLine(“——————————————————-”); Console。WriteLine($“{group。Key。Id} {group。Key。Name}, Total score: {group。Sum(x => x。Score)}”); foreach (var result in group) { Console。WriteLine($“{result。StudentId} {result。StudentName}\n{result。CourseName} {result。Score}”); }}

增加了

itemAction

,在最後將 key 的資訊再同步回 group 內的各個資料,此時我們再來執行一下我們的示例,結果如下:

實現一個基於相等性比較的 GroupBy

可以看到現在我們的資料就都有 Id 和 Name 了~~

More

我們也可以增加一個

IEqualityComparer

的過載來支援自定義的 comparer

public static IEnumerable> GroupByEquality(this IEnumerable source, Func keySelector, IEqualityComparer keyComparer, Action? keyAction = null, Action? itemAction = null) where TKey : notnull{ return GroupByEquality(source, keySelector, keyComparer。Equals, keyAction, itemAction);}

References

https://github。com/dotnet/runtime/blob/main/src/libraries/System。Linq/src/System/Linq/Grouping。cs

https://github。com/dotnet/runtime/blob/main/src/libraries/System。Linq/src/System/Linq/Lookup。cs

https://github。com/WeihanLi/WeihanLi。Common/blob/05ba92b5439bfa8623ae9b3133bf78daf4a8f6b4/src/WeihanLi。Common/Extensions/EnumerableExtension。cs#L275

https://github。com/WeihanLi/WeihanLi。Common/blob/dev/samples/DotNetCoreSample/GroupByEqualitySample。cs#L10

文章來源於amazingdotnet ,作者WeihanLi