前言
最近遇到一個需求,要將多欄位的資料分組,再將各分組的資料排序後,取出前 N 筆資料出來。
舉例假想情境如下:
- 有兩個隊伍:Blue Team 和 Red Team
- 比賽項目有三種:Black Jack、Poker Hand 和 Solitaire
- 需求:取出每一種比賽項目,每個隊伍最高分的資料
1 | public static void Main() |
印象中,三年前回鍋軟體開發,自學 C# 時有用 LINQ 做過類似的練習,之後一直沒機會遇到這種使用情境,時間久便忘記要怎麼寫了。
想不起來 LINQ 要怎麼寫,當下先用想到的方式解決,假日把 LINQ 的寫法也研究回來,現在整理成筆記,將來就不用再花時間重做功課了。
按照時間序,總共研究出三種寫法:
方法一:使用 HashSet 處理
方法二:使用 LINQ - ToHashSet() 處理
方法三:使用 LINQ - GroupBy() 與 SelectMany() 處理
不想看過程的話,可以直接跳到 方法三:使用 LINQ - GroupBy() 與 SelectMany() 處理
方法一:使用 HashSet 處理
這個做法用到之前在 C# 覆寫 Equals 方法,為何要覆寫 GetHashCode 方法 提過的規則,HashSet 在加入資料時,遇到重複會略過的特性。
實作的概念:
在 Player 類別覆寫 Equals() 和 GetHashCode() 方法,自訂判斷重複的檢查條件,讓 HashSet<Player> 在 Add() 時略過不要的資料即可。
將 List<Player> 先做好 Score 的降冪排序,再逐筆加進 HashSet<Player> 內,讓 HashSet 內只剩下每組 Team + Game 不重複的資料,且該筆資料是該分組最高分就完成了。
按照範例的需求,要用 Team 和 Game 屬性當做分組的條件,覆寫的方法如下:
1 | public class Player |
但我直覺認為不該覆寫 Player 類別,資料分組不屬於 Player 類別的工作。
我覺得可以再新增一個處理資料分組專用的 WinnerPlayer,讓他繼承 Player 所有屬性,然後覆寫 WinnerPlayer 類別的 Equals()、GetHashCode() 方法,再利用 HashSet<WinnerPlayer> 的 Add() 方法過濾資料,就不會污染 Player 類別了。
而處理 HashSet.Add() 的動作,又可以封裝到 ScoreFilter 類別內,建立 ScoreFilter 物件時,將 List<Player> 資料送進 ctor 建構式,再透過 GetWinner() 方法取得資料分組和取最高分的結果即可。
接著又想到因為外面用不到 WinnerPlayer 類別,可將 WinnerPlayer 宣告為私有類別,並封裝進 ScoreFilter 類別內。
綜合以上想法,得到的完成品如下:
1 | public static void Main() |
執行結果:
方法二:使用 LINQ - ToHashSet() 處理
這是方法一的改進版,在重構方法一的過程,想到 LINQ 有 ToHashSet() 方法,這個方法可傳入 IEqualityComparer<T>,就不需要 WinnerPlayer 類別了,程式碼簡化成這樣:
1 | public static void Main() |
相同的執行結果:
方法三:使用 LINQ - GroupBy() 與 SelectMany() 處理
最後找到以前寫的很雜亂的筆記,重新拼湊出 LINQ 的寫法了,這次連 IEqualityComparer<T> 都省了,並增加可指定要取得各組前 N 筆的參數。
1 | public static void Main() |
GroupBy 與 SelectMany 對應的操作
執行結果:
將 .Take(1) 改成 .Take(N) 就可以取出每組的前 N 筆資料,例如 .Take(2) 的結果如下:
改用 LINQ 來寫,程式碼簡短、要指定每個資料分組要取出幾筆資料,也很容易。