LINQ - SelectMany
艾倫家的地下室有三個抽屜,每個抽屜都要世界的秘密,我們想將它們都攤開
- 最外層是地下室
- 次一層表示抽屜們
- 下一層表示機密文件
Select
1 | [[照片,日記],[戰爭情報,國家情報],[美女照]] |
使用 Select,我們等於搬著三個抽屜離開
SelectMany
1 | [照片,日記,戰爭情報,國家情報,美女照] |
SelectMany,讓我們每一樣任務物品都可以輕易地取出排列整齊,還可以偷偷把美女照藏起來
🐛 如果世界上沒有 SelectMany
今天我們有這樣一個集合,有 7 個老師,每位老師有 3 名學生,共 21 個學生
1 |
|
需求是這樣,請把不及格的學生抓出來校長要照顧一下
1 |
|
結果可以拿到
但我們看到巢狀就頭暈貧血發作要怎麼辦
別急,我們還有 Query Expressions !
1 | var students = from t in teachers |
這種作法增加了 “SQL” 長期使用者的易讀性,描述了程式應該完成的任務 (就是所謂的 Declarative Programming),而不指定具體的步驟,也就是,閱讀程式碼的人一眼知道你想幹嘛而不是研究具體的步驟怎麼處理
由於我們因為太喜歡鏈式風格,我們不死心的嘗試 SELECT 兩次
但問題在這,他的維度還是停留在 teachers 這一層, SELECT 100 次也沒有用
1 |
|
等等,Aggregate 呢? 他讓我們自己定義一個邏輯,把一堆資料累加、合併、轉換成想要的結果。
1 |
|
喔…好累 ゚Å゚)
🐛 SelectMany
操作資料時,我們總有一堆集合,每個集合裡還有一堆7788 的東西,我想把所有東西攤開來處理
本質上:SelectMany = 「先選出多個東西」+「自動攤平成一個集合」
1 | teachers.SelectMany(t => t.Students).Where(s => s.Score < 60).ToList(); |
方法簽章
1 |
|
參數中,若傳入 resultSelector,讓我們同時處理 TSource(原本集合的每一個元素) 及 TCollection(子集合中的每一個元素)
1 | teachers.SelectMany(t => t.Students |
1 | [ |
SourceCode
其實 SelectMany 的本質還是:幫你把這「雙層迴圈」內建起來,我們看 sourcecode 便知
SelectMany 有 4 種 overload,為了因應軟體設計的本質之一 : 彈性
從一個集合的每一項取出「一組子集合」,然後攤平成一條清單(扁平化 flat),但我們在寫程式的時候,有時:
- 只需要子集合
- 有時候需要 原本那一項的 index
- 有時候還需要 原本那一項 + 子集合元素同時用來產生結果
| Overload 編號 | 方法簽章 | 支援 index? | 支援原始項目 + 子項目同時投影? | 常見用途 |
|---|---|---|---|---|
| #1 | SelectMany(Func<TSource, int, IEnumerable<TResult>> selector) |
✅ 是 | ❌ 否 | 需要知道元素位置(index)時 |
| #4 | SelectMany(Func<TSource, IEnumerable<TResult>> selector) |
❌ 否 | ❌ 否 | 最常見情況,純攤平子集合 |
| #2 | SelectMany(Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) |
❌ 否 | ✅ 是 | 想要保留父子關係進行投影 |
| #3 | SelectMany(Func<TSource, int, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) |
✅ 是 | ✅ 是 | 想要保留 index 與父子關係進行複雜處理 |
最基本版(最常見)
1 |
|
- 判斷 source 及 selector 是否為空,為空的話丟 ArgumentNull 例外
- 傳回 SelectManyIterator
- 真正的處理邏輯,被包在一個「私有的 iterator 方法」裡面。
1 |
|
這是一個 「iterator」方法,會用 yield return 把資料一筆一筆生出來。
- 初始化 index(因為第一次加一後才是 0)
- 外層迴圈:把 source 中的每個項目(例如老師)一筆一筆拿出來。
- 安全地幫 index 加 1,確保不會超出整數範圍(溢位會拋出例外)
- 這就是關鍵的「第二層迴圈」:
- selector 是你自己提供的轉換邏輯
- 它回傳的是一組子集合(例如學生清單)
- 所以你還要再迴圈一次,把子集合攤平
- 每一筆學生(子集合元素)都用 yield return 一筆一筆回傳。
🐛 使用 SelectMany 來處理關聯式資料比手動使用 JOIN 語法更好
在使用 Entity Framework(EF)操作資料庫時,當我們需要查詢「一對多」或「多對一」的關聯資料時,有兩種常見寫法:
🅰 傳統 SQL 思維 — 使用 join
1 | var queryB = from s in context.Schools |
🅱 更現代 LINQ 思維 — 使用 SelectMany 搭配導覽屬性
1 | var queryA = context.Schools.SelectMany(s => s.Teachers); |
哪一種方式比較好?在大資料量的情況下,效能會差很多嗎?使用 SelectMany 是不是只是寫法漂亮,但實務上沒效率?
先說結論,在 Entity Framework 中,優先使用 SelectMany 搭配「導覽屬性(Navigation Properties)」的方式通常是更好、更乾淨、更推薦的寫法。
語意更貼近物件導向
1 | school.Teachers |
這樣的寫法表達的是:「我從每個學校中,拿出它的老師們」
這種語意清楚又直觀,不需要你去關心「外鍵欄位叫什麼」,你只需要透過模型之間的導覽屬性操作即可。
EF 會自動產生 JOIN,效能不會比較差
SelectMany
1 | var teachers = context.Schools |
EF 背後會根據導覽屬性與關聯的中繼資料,自動產生SQL
JOIN
1 | var teachers = from s in context.Schools |
功能一樣,但 SelectMany 讀起來更像是在查詢「物件的資料」,而不是「操作資料表」。
雖然 SelectMany 通常是更好的選擇,但有些情況下 join 還是必要的,例如
- 你要查詢兩個彼此沒有導覽屬性的資料表
- 你要查詢左外連接(Left Join)
- 你只想查出欄位(非物件)或匿名類型組合資料
🐛 參考文章
SelectMany的應用Chain SelectMany instead of using a JOIN statement




