Grouping Operators - GroupBy

Grouping Operators - GroupBy

原始文章將記錄於此
https://github.com/s0920832252/LinQ-Note

碎碎念

說老實話 , 研讀文件之後 , 我發現 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

使用 GroupBy 時需要指定分組的 Key ( 通常是成員的某個屬性 ) , 以此屬性做為分組的依據 

多載

依照 elementSelector , resultSelector , comparer 的有無 , 共有八個多載 , 此處使用 comparer 作為劃分 , 分成四組.

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. 此兩個方法的差在只在於是否使用自定義比較器 , 若不使用 , 會使用預設比較器
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 成員去計算結果. 此時可考慮使用以下四種多載

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 .
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 的使用方式

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();
}
輸出結果

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();
        }
輸出結果

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();
}
輸出結果

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();
}
輸出結果

簡單實作自己的 GroupBy

會使用到 Lookup , 請參考這邊文章 LinQ基礎 - Lookup

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簡單介紹
Grouping.cs
Enumerable.cs
C#的利器LINQ-GroupBy的原碼探索
C#的利器LINQ-GroupBy的應用
[C#]LINQ–GroupBy 群組
利用LINQ GroupBy快速分組歸類
C# LINQ: GroupBy

Thank you!

You can find me on

若有謬誤 , 煩請告知 , 新手發帖請多包涵

:100: :muscle: :tada: :sheep: