1. 前言
想像一下有兩疊信:第一疊是寫有名字的信封,第二疊是要寄給這些人的明信片。你的任務是把每個信封跟第二疊裡同樣位置的明信片配對。第一個信封配第一張明信片,第二個配第二張,以此類推。你就這樣一邊走一邊從兩疊各拿一個元素,把它們「zip」在一起。
在程式裡這種操作就叫 zip —— 就像拉鍊一樣,把兩邊「咬」在一起,完全照順序來。
在 LINQ 裡這是一個很方便又強大的工具,可以把兩個(有時甚至更多)序列的資料合併起來,當你很在意順序的時候:第一個配第一個,第二個配第二個,依此類推。
要記得: Zip 只會合併到其中一個集合沒東西為止。如果有一個比較短,結果就會被截斷到最短的那個長度。
語法跟工作原理
Zip 會拿兩個(或更多)list,把它們依照 index 合成一個新的:第 0 個配第 0 個,第 1 個配第 1 個,依此類推。如果其中一個 list 先沒了,後面就不會再「zip」新對了——結果長度就是最短的那個。
主要簽名:
IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> resultSelector
)
注意:Zip 回傳的不只是 pair,而是你在 resultSelector 裡指定的東西。可以是 tuple、string、object,反正你想怎麼組都行——完全看你的邏輯。
- first 跟 second —— 要合併的集合。
- resultSelector —— 這個 function 會拿到兩個集合的元素,然後回傳你要的結果。
簡單說:同時走兩個集合,每一步把它們的 pair 做成你想要的東西。
2. Zip 用法範例——從簡單到進階
最簡單的例子:兩個數字陣列相加
假設我們有兩個 list:
- 數學分數
- 文學分數
// 兩個學生分數陣列
int[] mathScores = { 5, 4, 3, 5, 2 };
int[] literatureScores = { 4, 5, 3, 4, 3 };
// 依位置合併然後相加
var totalScores = mathScores.Zip(literatureScores, (math, literature) => math + literature);
foreach (var score in totalScores)
Console.WriteLine($"學生總分:{score}");
學生總分:9
學生總分:9
學生總分:6
學生總分:9
學生總分:5
你看,很直觀:第一個學生拿到第一個分數的總和,第二個拿第二個,以此類推。
組合字串跟數字:產品加上價格
假設你有一個產品清單跟一個價格清單。要輸出:"ProductName — Price"。
string[] productNames = { "蘋果", "梨子", "香蕉" };
decimal[] productPrices = { 50.5m, 60.0m, 35.2m };
var info = productNames.Zip(productPrices, (name, price) => $"{name} — {price} 歐元");
foreach (var s in info)
Console.WriteLine(s);
蘋果 — 50.5 歐元
梨子 — 60.0 歐元
香蕉 — 35.2 歐元
用物件集合
假設之前例子裡我們有一個 Student class 跟學生的集合。現在有一個 ratings 陣列,順序對應:
public class Student
{
public string Name { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Name = "瓦夏" },
new Student { Name = "彼佳" },
new Student { Name = "瑪莎" }
};
int[] ratings = { 7, 9, 8 };
var studentsWithRatings = students.Zip(ratings, (student, rating) =>
$"{student.Name}: 評分 {rating}");
foreach (var s in studentsWithRatings)
Console.WriteLine(s);
瓦夏: 評分 7
彼佳: 評分 9
瑪莎: 評分 8
合併更複雜的物件
現實裡常常要把兩個不同系統的訂單清單合併:每個訂單配對一個處理結果或狀態。
string[] orders = { "A-1", "B-2", "C-3" };
string[] statuses = { "已完成", "處理中", "已拒絕" };
var orderStatusList = orders.Zip(statuses, (order, status) => new { Order = order, Status = status });
foreach (var os in orderStatusList)
Console.WriteLine($"訂單 {os.Order}: {os.Status}");
訂單 A-1: 已完成
訂單 B-2: 處理中
訂單 C-3: 已拒絕
4. 重要細節與常見錯誤
用 Zip 的時候要記得:最後的集合長度會跟最短的輸入一樣。
比如你有 10 個學生,但只有 7 個分數,結果只會有7 個元素。這很常讓人覺得資料「不見」了。
錯誤一:集合長度或順序不一致。
如果你不小心把 list 順序搞錯,或是沒先排好讓元素一一對應,最後的 pair 會亂掉。
想像一下:學生清單跟分數順序不一樣——瓦夏可能拿到彼佳的分數。就像穿錯襪子:雖然都有穿,但感覺怪怪的。
錯誤二:其中一個集合是空的。
只要有一個 list 是空的,結果就會是空的。
Zip 只會在兩個序列都有東西時才工作。如果其中一個沒資料,你就會拿到「什麼都沒有」。
5. 多集合 Zip
一開始 LINQ 只支援合併兩個集合。但從 .NET 6 開始,多了一個 overload,可以一次 zip 三個(酷吧!)。
三個集合的例子:
string[] names = { "芮伊", "路克", "莉亞" };
string[] planets = { "塔圖因", "塔圖因", "奧德蘭" };
int[] ages = { 19, 23, 19 };
// .NET 6+ 新 overload:
var characters = names.Zip(planets, ages, (name, planet, age) =>
$"{name} ({planet}), {age} 歲");
foreach (var c in characters)
Console.WriteLine(c);
芮伊 (塔圖因), 19 歲
路克 (塔圖因), 23 歲
莉亞 (奧德蘭), 19 歲
結果的元素數量——還是看最短的 list!如果 ages 少一個,最後一個路克就「沒年齡」了。
6. Zip 跟 LINQ Query Syntax:有嗎?
你可能發現我們一直用「方法」語法(Method Syntax)。LINQ Query Syntax 沒有專門給 Zip 的關鍵字,不能用 from ... in ... 來做。原因很簡單:「zip」這個概念是依位置配對,而 LINQ 查詢語法主要是用 join、select 等等,通常是依 key 來「連結」,不是依位置。
結論:Zip 只能用點(就是 method)語法:
var zipped = collection1.Zip(collection2, (a, b) => ...);
如果你真的很想用 query syntax——建議還是不要啦。如果真的超想,也可以自己寫 extension method。🙂
7. Zip 在實際應用裡的用法
你的實戰例子:比對舊價跟新價
假設我們的學習 app 有一個產品清單,然後會不定期收到新價格。要輸出每個商品價格怎麼變化:
string[] productNames = { "蘋果", "香蕉", "鳳梨" };
decimal[] oldPrices = { 80.0m, 30.0m, 110.0m };
decimal[] newPrices = { 75.0m, 33.0m, 120.0m };
var priceChanges = productNames
.Zip(oldPrices, newPrices, (name, oldPrice, newPrice) =>
$"{name}: 原本 {oldPrice}, 現在 {newPrice}, 變化: {newPrice - oldPrice:+#;-#;0}");
foreach (var s in priceChanges)
Console.WriteLine(s);
蘋果: 原本 80, 現在 75, 變化: -5
香蕉: 原本 30, 現在 33, 變化: +3
鳳梨: 原本 110, 現在 120, 變化: +10
實戰:合併不同來源的結果
在「實戰」專案裡,Zip 常常用來合併不同 API 或資料表的結果,只要資料順序是外部保證的(比如天氣預報陣列跟實際測量值)。
8. Zip 在 LINQ 鏈式操作裡:跟其他 operator 組合
Zip 很常不是單獨用,而是 LINQ 長鏈的一部分。比如你可以先 filter list,再 zip,然後算統計。
var passedMath = mathScores.Where(x => x >= 3);
var passedLit = literatureScores.Where(x => x >= 3);
var passedPairs = passedMath.Zip(passedLit, (m, l) => m + l);
var avgScore = passedPairs.Average();
Console.WriteLine($"通過者的平均總分:{avgScore}");
小提醒:如果 Where 之後其中一個 list 變短,Zip 會依最短的來截斷。
Zip 跟其他合併方法比較
| 方法 | 重點 | 合併依據… | 結果長度… |
|---|---|---|---|
|
依 index 合併元素 | index(位置) | 最短的集合 |
|
依 key 合併 | 共同 key | 所有符合的 pair |
|
每個 key group 一個集合 | key | 以第一個集合為主 |
|
把集合接在一起 | 直接接到後面 | 長度總和 |
Zip 是位置對位置。 Join 則是依某個共同值(比如 code 或 id)來對。
9. 實用建議與常見錯誤
你在自己的 app 用 Zip 的時候,最重要的是要確定:
- 兩個集合的順序一致:不然會「湊」出瓦夏跟彼佳的分數。
- 集合有正確排序或事先準備好同步走訪。
- 結果一定是最短集合的長度。
- 如果你要依 key 對資料,不要用 Zip!那要用 Join。
- 還有一個常見錯誤——一個集合有重複,另一個沒有。或是 filter 過程中順序變了,對應就亂掉了。
GO TO FULL VERSION