--- tags: LinQ , C# , Grouping Operators --- # GroupBy ##### 碎碎念 說老實話 , 研讀文件之後 , 我發現 GroupBy 比我本來以為的還要複雜許多... 我以前只會使用最簡單的形式呀 Orz ### 前言 有時候我們會需要將資料依照組別擺放 , 以便日後查詢使用 e.g. 資料內有多個客戶的銷售紀錄 , 將同一個客戶的銷售紀錄存在一個 List<銷售紀錄> 內 , 並使用客戶名作為 Key , 該客戶的 List<銷售紀錄> 作為 value , 存入一個 Dictinary<客戶名,List<銷售紀錄>>. 在不使用 GroupBy 的情況下 , 需要自己走訪每一筆資料 , 並查看該資料使否存在於 Dictinary 內 , 若無 , 則 new 一個 List<T> 並將該資料存入 , 確保該 Key 對應一個 List<T> 在 Dictionary 內. 之後就只要透過 Key 即可查詢對應的 List<T>資料 . 但使用 GroupBy 後 , 則不需要執行上述動作. 一言以蔽之 , GroupBy 可以幫你將資料依照某個條件分群. ##### [GroupBy](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/linq/grouping-data) 使用 GroupBy 時需要指定分組的 Key ( 通常是成員的某個屬性 ) , 以此屬性做為分組的依據 ![4d4pLmU.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/4d4pLmU.png?raw=true) ### [多載](https://docs.microsoft.com/zh-tw/dotnet/api/system.linq.enumerable.groupby?view=netframework-4.8) 依照 elementSelector , resultSelector , comparer 的有無 , 共有八個多載 , 此處使用 comparer 作為劃分 , 分成四組. ```C# public static IEnumerable<IGrouping<TKey,TSource>> GroupBy<TSource,TKey> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector ) public static IEnumerable<IGrouping<TKey,TSource>> GroupBy<TSource,TKey> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, IEqualityComparer<TKey> comparer ) ``` - 傳入參數 - keySelector : 指定以甚麼屬性作為分組的依據 - comparer : 自定義比較器 , 用來比較兩個 key 是否相同. 以決定是否分在同一組 - 回傳值 - 型態 : IEnumerable<IGrouping<TKey, **TSource**>> - 由型態可知 , 回傳值是 IGrouping<TKey, TSource> 的集合 - IGrouping<TKey, TSource> 是分組後的資料 , 每一個 IGrouping 會有一個 TKey Key 以及與該 Key 相對應的 TSource Value. - 總結 : 1. 使用 keySelector 設定要使用哪一個 Key 作為資料集合分組的依據 , 然後以此依據分組並輸出成分組後資料的資料集合 IEnumerable<IGrouping<TKey, TSource>> 2. 此兩個方法的差在只在於是否使用自定義比較器 , 若不使用 , 會使用[預設比較器](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.comparer-1.default?view=netframework-4.8)。 ```C# public static IEnumerable<IGrouping<TKey,TElement>> GroupBy<TSource,TKey,TElement> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TSource,TElement> elementSelector ) public static IEnumerable<IGrouping<TKey,TElement>> GroupBy<TSource,TKey,TElement> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TSource,TElement> elementSelector, IEqualityComparer<TKey> comparer ) ``` - 傳入參數 - keySelector : 指定以甚麼屬性作為分組的依據 - comparer : 自定義比較器 , 用來比較兩個 key 是否相同. 以決定是否分在同一組 - elementSelector : 決定資料成員最後回傳的結果 , 可以想成是 Select . - 回傳值 - 型態 : IEnumerable<IGrouping<TKey,**TElement**>> - 由型態可知 , 回傳值是 IGrouping<TKey, TElement> 的集合 - 總結 : - 上一組沒有 elementSelector , 所以其預設是回傳資料成員自身 , 並無任何轉換. 而此組因為其回傳值會透過 elementSelector 轉換後才回傳 , 所以回傳值型態為 IEnumerable<IGrouping<TKey,TElement>> > 有時候 , 若想要直接拿到 Group 的某項數值而非 Group 時 , 但以上四個多載 , 其回傳值型態都是 IGrouping 的集合. 因此還必須再走訪該 Group 成員去計算結果. 此時可考慮使用以下四種多載 ```C# public static IEnumerable<TResult> GroupBy<TSource,TKey,TResult> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TKey,IEnumerable<TSource>,TResult> resultSelector ) public static IEnumerable<TResult> GroupBy<TSource,TKey,TResult> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TKey,IEnumerable<TSource>,TResult> resultSelector, IEqualityComparer<TKey> comparer ) ``` - 傳入參數 - keySelector : 指定以甚麼屬性作為分組的依據 - comparer : 自定義比較器 , 用來比較兩個 key 是否相同. 以決定是否分在同一組 - resultSelector : 決定分組後的每一組資料集合應該如何轉換成某個結果並回傳. - 回傳值 - 型態 : IEnumerable<TResult> - 總結 : - 回傳值不再是 IEnumerable<IGrouping<,>> , 因為 IGrouping 已經被 resultSelector 轉換成 TResult . ```C# public static IEnumerable<TResult> GroupBy<TSource,TKey,TElement,TResult> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TSource,TElement> elementSelector, Func<TKey,IEnumerable<TElement>,TResult> resultSelector ) public static IEnumerable<TResult> GroupBy<TSource,TKey,TElement,TResult> ( this IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TSource,TElement> elementSelector, Func<TKey,IEnumerable<TElement>,TResult> resultSelector, IEqualityComparer<TKey> comparer ) ``` - 傳入參數 - keySelector : 指定以甚麼屬性作為分組的依據 - comparer : 自定義比較器 , 用來比較兩個 key 是否相同. 以決定是否分在同一組 - elementSelector : 決定資料成員傳給 resultSelector 的結果. - resultSelector : 決定分組後的每一組資料集合應該如何轉換成某個結果並回傳. - 回傳值 - 型態 : IEnumerable<TResult> ### GroupBy 的使用方式 ```C# static void Main(string[] args) { List<(string petName, double petAge)> petCollection = new List<(string petName, double petAge)>() { (petName:"小豬", petAge:5.1), (petName:"大黃", petAge:5.9), (petName:"小龜", petAge:5.3), (petName:"小牛", petAge:4.3), (petName:"小馬", petAge:4.9), (petName:"小龍", petAge:7), }; var queryResult = petCollection.GroupBy(pet => Math.Floor(pet.petAge)); foreach (var petGroup in queryResult) { Console.WriteLine($"使用的 key 是 {petGroup.Key}"); foreach (var (petName, petAge) in petGroup) { Console.WriteLine($"寵物名 : {petName} 寵物年紀 : {petAge}"); } Console.WriteLine("====================================="); } Console.ReadKey(); } ``` ##### 輸出結果 ![eg0fgKj.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/eg0fgKj.png?raw=true) ```C# static void Main(string[] args) { List<(string petName, double petAge)> petCollection = new List<(string petName, double petAge)>() { (petName:"小豬", petAge:5.1), (petName:"大黃", petAge:5.9), (petName:"小龜", petAge:5.3), (petName:"小牛", petAge:4.3), (petName:"小馬", petAge:4.9), (petName:"小龍", petAge:7), }; var queryResult = petCollection.GroupBy(pet => Math.Floor(pet.petAge), pet => $"寵物名稱是 {pet.petName} , 寵物年紀是 {pet.petAge}"); foreach (var petGroup in queryResult) { Console.WriteLine($"使用的 key 是 {petGroup.Key}"); foreach (var petStr in petGroup) { Console.WriteLine(petStr); } Console.WriteLine("====================================="); } Console.ReadKey(); } ``` ##### 輸出結果 ![2Ewg17z.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/2Ewg17z.png?raw=true) ```C# static void Main(string[] args) { List<(string petName, double petAge)> petCollection = new List<(string petName, double petAge)>() { (petName:"小豬", petAge:5.1), (petName:"大黃", petAge:5.9), (petName:"小龜", petAge:5.3), (petName:"小牛", petAge:4.3), (petName:"小馬", petAge:4.9), (petName:"小龍", petAge:7), }; var queryResult = petCollection.GroupBy(pet => Math.Floor(pet.petAge), (key, petGroup) => ( Key: key, PetCount: petGroup.Count(), PetAverage: petGroup.Average(pet => pet.petAge), PetSum: petGroup.Sum(pet => pet.petAge) ) ); foreach (var (Key, PetCount, PetAverage, PetSum) in queryResult) { Console.WriteLine($"key : {Key}"); Console.WriteLine($"Count : {PetCount}"); Console.WriteLine($"Average : {PetAverage}"); Console.WriteLine($"Sum : {PetSum}"); Console.WriteLine("====================================="); } Console.ReadKey(); } ``` ##### 輸出結果 ![1m9jONZ.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/1m9jONZ.png?raw=true) ```C# static void Main(string[] args) { List<(string petName, double petAge)> petCollection = new List<(string petName, double petAge)>() { (petName:"小豬", petAge:5.1), (petName:"大黃", petAge:5.9), (petName:"小龜", petAge:5.3), (petName:"小牛", petAge:4.3), (petName:"小馬", petAge:4.9), (petName:"小龍", petAge:7), }; var queryResult = petCollection.GroupBy(pet => Math.Floor(pet.petAge), pet => pet.petAge, (key, ages) => ( Key: key, PetCount: ages.Count(), PetAverage: ages.Average(), PetSum: ages.Sum() ) ); foreach (var (Key, PetCount, PetAverage, PetSum) in queryResult) { Console.WriteLine($"key : {Key}"); Console.WriteLine($"Count : {PetCount}"); Console.WriteLine($"Average : {PetAverage}"); Console.WriteLine($"Sum : {PetSum}"); Console.WriteLine("====================================="); } Console.ReadKey(); } ``` ##### 輸出結果 ![JyIQaa5.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/JyIQaa5.png?raw=true) ### 簡單實作自己的 GroupBy 會使用到 Lookup , 請參考這邊文章 [LinQ基礎 - Lookup](https://hackmd.io/yMO0aHHPQKm61t0ceAeDCQ) ```C# public static IEnumerable<IGrouping<TKey, TSource>> MyGroupBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) { return Lookup<TKey, TSource>.Create<TSource>(source, keySelector, (element) => element, null); } public static IEnumerable<IGrouping<TKey, TSource>> MyGroupBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer) { return Lookup<TKey, TSource>.Create<TSource>(source, keySelector, (element) => element, comparer); } public static IEnumerable<IGrouping<TKey, TElement>> MyGroupBy<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) { return Lookup<TKey, TElement>.Create<TSource>(source, keySelector, elementSelector, null); } public static IEnumerable<IGrouping<TKey, TElement>> MyGroupBy<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer) { return Lookup<TKey, TElement>.Create<TSource>(source, keySelector, elementSelector, comparer); } public static IEnumerable<TResult> MyGroupBy<TSource, TKey, TResult>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, IEnumerable<TSource>, TResult> resultSelector) { Lookup<TKey, TSource> lookup = Lookup<TKey, TSource>.Create<TSource>(source, keySelector, (element) => element, null); return lookup.ApplyResultSelector(resultSelector); } public static IEnumerable<TResult> MyGroupBy<TSource, TKey, TElement, TResult>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, Func<TKey, IEnumerable<TElement>, TResult> resultSelector) { Lookup<TKey, TElement> lookup = Lookup<TKey, TElement>.Create<TSource>(source, keySelector, elementSelector, null); return lookup.ApplyResultSelector(resultSelector); } public static IEnumerable<TResult> MyGroupBy<TSource, TKey, TResult>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, IEnumerable<TSource>, TResult> resultSelector, IEqualityComparer<TKey> comparer) { Lookup<TKey, TSource> lookup = Lookup<TKey, TSource>.Create<TSource>(source, keySelector, (element) => element, comparer); return lookup.ApplyResultSelector(resultSelector); } public static IEnumerable<TResult> MyGroupBy<TSource, TKey, TElement, TResult>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, Func<TKey, IEnumerable<TElement>, TResult> resultSelector, IEqualityComparer<TKey> comparer) { Lookup<TKey, TElement> lookup = Lookup<TKey, TElement>.Create<TSource>(source, keySelector, elementSelector, comparer); return lookup.ApplyResultSelector(resultSelector); } ``` ### 總結 - GroupBy 的流程 - 實際上 GroupBy 的程式碼會去建立一個走訪器 Enumerable. 不過我偷懶 , 不實作 , 若有興趣 , 請檢閱下方參考有關於 GroupBy 的程式碼. 1. 建立相對應的走訪器 2. 走訪器會去使用 Lookup.Create() 得到 group 資料 - 使用 GetGrouping() 對於 group 的新增或查詢(找 group ) - 使用 Add() 將元素將入 group 中 3. 依序走訪各個 group. - 依據 resultSelector 的有無 , 而去使用不同的方法. - 若有 , 使用 ApplyResultSelector() - 若無 , 使用 GetEnumerator() ### 參考 [[C#] ToLookup, GroupBy, ToDictionary簡單介紹](https://dotblogs.com.tw/kirkchen/2011/07/16/toolookup_groupby_todicionary_introduction) [Grouping.cs](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Linq/src/System/Linq/Grouping.cs) [Enumerable.cs](https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs) [C#的利器LINQ-GroupBy的原碼探索](https://ithelp.ithome.com.tw/articles/10196274) [C#的利器LINQ-GroupBy的應用](https://ithelp.ithome.com.tw/articles/10196181) [[C#]LINQ–GroupBy 群組](https://kw0006667.wordpress.com/2013/05/31/clinqgroupby-%E7%BE%A4%E7%B5%84/) [利用LINQ GroupBy快速分組歸類](https://blog.darkthread.net/blog/linq-groupby-todictionary-grouping/) [C# LINQ: GroupBy](https://jasper-it.blogspot.com/2015/01/c-linq-groupby.html) ### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: