利用LINQ GroupBy快速分組歸類

分享最近學到的LINQ小技巧一則。有時我們會需求將資料物件分組擺放,方便後續查詢處理,例如:將散亂的銷售資料依客戶分群,同一客戶的所有資料變成一個List<T>。

過去面對這種問題,我慣用的做法先定義一個Dictionary<string, List<T>>,使用 foreach 逐筆抓取來源資料,從中取出鍵值(例如:客戶編號),先檢查鍵值是否已存在於Dictionary,若無則新増一筆並建立空的List<T>,確保Dictionary有該鍵值專屬List<T>,將資料放入List<T>。執行完畢得到以鍵值分類的List<T>,再進行後續處理。

foreach + Dictionary寫法用了好幾年,前幾天才忽然想到,這不就是SQL語法中的GROUP BY嗎?加上LINQ有ToDictionary, GroupBy(o => o.客戶編號).ToDictionary(o => o.Key, o => o.ToList()) 一行就搞定了呀!阿呆。

來個應景的程式範例吧!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace LinqTip
{
    class Program
    {
        public enum Teams
        {
            Valor, Mystic, Instinct, Dark
        }
 
        public class Trainer
        {
            public Teams Team;
            public string Name;
            public Trainer(Teams team, string name)
            {
                Team = team; Name = name;
            }
        }
 
        static void Main(string[] args)
        {
            //來源資料如下
            List<Trainer> trainers = new List<Trainer>()
            {
                new Trainer(Teams.Valor, "Candela"),
                new Trainer(Teams.Valor, "Bob"),
                new Trainer(Teams.Mystic, "Blanche"),
                new Trainer(Teams.Valor, "Alice"),
                new Trainer(Teams.Instinct, "Spark"),
                new Trainer(Teams.Mystic, "Tom"),
                new Trainer(Teams.Dark, "Jeffrey")
            };
            //目標:以Team分類,將同隊的訓練師集合成List<Trainer>,
            //最終產出Dictionary<Teams, List<Trainer>>
 
            //以前的寫法,跑迴圈加邏輯比對
            var res1 = new Dictionary<Teams, List<Trainer>>();
            foreach (var t in trainers)
            {
                if (!res1.ContainsKey(t.Team))
                    res1.Add(t.Team, new List<Trainer>());
                res1[t.Team].Add(t);
            }
 
            //新寫法,使用LINQ GroupBy
            var res2 =
                trainers.GroupBy(o => o.Team)
                .ToDictionary(o => o.Key, o => o.ToList());
        }
    }
}

就醬,又學會一招~

不過,GroupBy().ToDictionary() 做法適用分類現有資料,若之後要陸續接收新增資料,仍可回歸 foreach + Dictionary<string, List<T>> 寫法。

[2016-08-24補充] 感謝Phoenix補充,LINQ還有更簡潔的做法:ToLookup(o > o.Teams, o => o),其產出的型別為ILookup,以Key分組的Value集合,與Dictionary最大的差異是ILookup屬唯讀性質,事後不能變更或修改集合項目。

歡迎推文分享:
Published 24 August 2016 07:59 AM 由 Jeffrey
Filed under:
Views: 17,834



意見

# Phoenix said on 23 August, 2016 08:55 PM

另一種寫法

var res3 = trainers.ToLookup(o => o.Team, o => o);

# 小安 said on 23 August, 2016 09:41 PM

倒數第二行的 OrderBy().ToDictionary()

是筆誤 原本是GroupBy().ToDictionary() 嗎 ?

# Jeffrey said on 23 August, 2016 11:05 PM

to Phoenix, 學習了!感謝補充,已加入本文。

to 小安,是的,我又寫錯了(大概是早上還沒睡飽 Orz),謝謝指正。

# Holey said on 24 August, 2016 06:44 AM

第三段開頭是不是筆誤呢 (foreah -> foreach )

# Jeffrey said on 24 August, 2016 08:50 AM

to Holey, 西滴,謝謝指正。

# Jack said on 22 December, 2016 06:26 AM

如果想要自訂群組可以用類似

string[] StringSet={ 字串集合…… };

var query1=StringSet.GroupBy(s=>s,new StringComparer());

那如果不想用Lambda運算式 想要用 查詢運算式

var query2=from S in StringSet

group S by new StringComparer();

但這樣跑出的結果很奇怪 請問要如何修改?

# Jeffrey said on 22 December, 2016 07:03 PM

to Jack, 較常見的GroupBy應用是每筆資料有多個欄位,依其中某個欄位對資料做分組,不太明白你所說將單純字串陣列做GroupBy的情境,能再提供更具體的範例嗎?

# Jack said on 23 December, 2016 05:50 AM

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace Test_String_Group_

{

   class StringComparer:IEqualityComparer<string>

   {

       public bool Equals(string x,string y)

       {

           return GetHashCode(x) == GetHashCode(y);

       }

       public int GetHashCode(string String)

       {

           return String.Length;

       }

   }

   class Program

   {

       static void Main(string[] args)

       {

           string[] S = { "A12", "A123", "B12", "C12", "B123", "A1234", "B1234", "C123", "C1234" };

           var query = from s in S

                       group s by new StringComparer();

           /*var query = S.GroupBy(s => s, new StringComparer());*/

           foreach(var group in query)

           {

               Console.WriteLine(group.Key + " : ");

               foreach(var item in group)

               {

                   Console.WriteLine(item);

               }

           }

           Console.Read();

       }

   }

}

用Lambda 執行的結果是正確的

但用LINQ執行出來是錯的

# Jeffrey said on 26 December, 2016 04:42 PM

to Jack, 依我所知,GroupBy() 允許自訂 IEqualityComparer,group by 後方接的應是比對值而不是比對邏輯物件,所以結果才會跟你預期的不一樣。如果一定要寫成類 SQL 語法,我想到最接近的解法是寫成 var query = from s in S group s by s.Length。

# Jack said on 13 January, 2017 09:45 PM

如果一定要用IEqualityComparer自訂群組

且用類似SQL的寫法

要如何寫

# Jeffrey said on 14 January, 2017 01:58 AM

to Jack, 我個人的看法是無解,即使有解,其複雜度與成本應會令人卻步。

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<August 2016>
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication