CodeGym /課程 /C# SELF /Zip 依位置合併

Zip 依位置合併

C# SELF
等級 33 , 課堂 4
開放

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,反正你想怎麼組都行——完全看你的邏輯。

  • firstsecond —— 要合併的集合。
  • 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 查詢語法主要是用 joinselect 等等,通常是依 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 跟其他合併方法比較

方法 重點 合併依據… 結果長度…
Zip
依 index 合併元素 index(位置) 最短的集合
Join
依 key 合併 共同 key 所有符合的 pair
GroupJoin
每個 key group 一個集合 key 以第一個集合為主
Concat
把集合接在一起 直接接到後面 長度總和

Zip 是位置對位置。 Join 則是依某個共同值(比如 code 或 id)來對。

9. 實用建議與常見錯誤

你在自己的 app 用 Zip 的時候,最重要的是要確定:

  • 兩個集合的順序一致:不然會「湊」出瓦夏跟彼佳的分數。
  • 集合有正確排序或事先準備好同步走訪。
  • 結果一定是最短集合的長度。
  • 如果你要依 key 對資料,不要用 Zip!那要用 Join
  • 還有一個常見錯誤——一個集合有重複,另一個沒有。或是 filter 過程中順序變了,對應就亂掉了。
1
問卷/小測驗
集合合併,等級 33,課堂 4
未開放
集合合併
進階 LINQ:合併與投影
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION