1. Giới thiệu
Hãy tưởng tượng có hai chồng thư: chồng đầu là phong bì có ghi tên, chồng thứ hai là bưu thiếp cho đúng người nhận. Nhiệm vụ của bạn là ghép từng phong bì với bưu thiếp ở cùng vị trí trong chồng thứ hai. Phong bì đầu với bưu thiếp đầu, phong bì thứ hai với bưu thiếp thứ hai, v.v. Bạn đi dọc hai chồng, lấy từng phần tử từ mỗi chồng và "zip" chúng lại với nhau.
Trong lập trình, thao tác này gọi là zip — giống như khóa kéo, "kẹp" hai bên lại với nhau, từng vị trí một.
Trong LINQ, đây là công cụ tiện lợi và mạnh mẽ để kết hợp dữ liệu từ hai (đôi khi nhiều hơn) sequence, khi thứ tự là quan trọng: phần tử đầu với phần tử đầu, phần tử hai với phần tử hai, v.v.
Lưu ý: Zip chỉ hoạt động khi cả hai collection còn phần tử. Nếu một collection ngắn hơn — kết quả sẽ bị cắt theo collection ngắn nhất.
Cú pháp và nguyên lý hoạt động
Zip lấy hai (hoặc nhiều) list và kết hợp chúng thành list mới: các phần tử được ghép theo index, tức là 0 với 0, 1 với 1, v.v. Nếu một list hết sớm hơn, các cặp mới sẽ không được "zip" nữa — kết quả sẽ có độ dài bằng list ngắn nhất.
Chữ ký (cơ bản):
IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> resultSelector
)
Lưu ý: Zip trả về không chỉ là cặp, mà là cái bạn chỉ định trong resultSelector. Có thể là tuple, string, object hay bất cứ gì — tuỳ logic của bạn.
- first và second — collection để kết hợp.
- resultSelector — function nhận từng phần tử của hai collection và trả về kết quả mới.
Nói đơn giản: đi qua hai collection cùng lúc, mỗi bước tạo ra cái mình cần từ cặp phần tử.
2. Ví dụ dùng Zip — từ dễ đến khó
Ví dụ đơn giản nhất: cộng hai mảng số
Giả sử bạn có hai list:
- Điểm toán
- Điểm văn
// Hai mảng điểm của sinh viên
int[] mathScores = { 5, 4, 3, 5, 2 };
int[] literatureScores = { 4, 5, 3, 4, 3 };
// Ghép theo vị trí và cộng lại
var totalScores = mathScores.Zip(literatureScores, (math, literature) => math + literature);
foreach (var score in totalScores)
Console.WriteLine($"Tổng điểm sinh viên: {score}");
Tổng điểm sinh viên: 9
Tổng điểm sinh viên: 9
Tổng điểm sinh viên: 6
Tổng điểm sinh viên: 9
Tổng điểm sinh viên: 5
Nhìn xem, rất logic: sinh viên đầu tiên lấy tổng điểm đầu, sinh viên thứ hai lấy tổng điểm thứ hai, v.v.
Kết hợp string và số: gắn giá cho sản phẩm
Giả sử bạn có list sản phẩm và list giá. Cần in ra: "TênSảnPhẩm — Giá".
string[] productNames = { "Táo", "Lê", "Chuối" };
decimal[] productPrices = { 50.5m, 60.0m, 35.2m };
var info = productNames.Zip(productPrices, (name, price) => $"{name} — {price} euro");
foreach (var s in info)
Console.WriteLine(s);
Táo — 50,5 euro
Lê — 60,0 euro
Chuối — 35,2 euro
Dùng với collection object
Giả sử trước đó bạn có class Student và list sinh viên. Bây giờ — một mảng rating riêng, ghép theo thứ tự:
public class Student
{
public string Name { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Name = "Vasya" },
new Student { Name = "Petya" },
new Student { Name = "Masha" }
};
int[] ratings = { 7, 9, 8 };
var studentsWithRatings = students.Zip(ratings, (student, rating) =>
$"{student.Name}: xếp hạng {rating}");
foreach (var s in studentsWithRatings)
Console.WriteLine(s);
Vasya: xếp hạng 7
Petya: xếp hạng 9
Masha: xếp hạng 8
Kết hợp object phức tạp hơn
Thực tế thường cần ghép hai list đơn hàng từ hai hệ thống khác nhau: mỗi đơn hàng ghép với trạng thái xử lý.
string[] orders = { "A-1", "B-2", "C-3" };
string[] statuses = { "Hoàn thành", "Đang xử lý", "Từ chối" };
var orderStatusList = orders.Zip(statuses, (order, status) => new { Order = order, Status = status });
foreach (var os in orderStatusList)
Console.WriteLine($"Đơn hàng {os.Order}: {os.Status}");
Đơn hàng A-1: Hoàn thành
Đơn hàng B-2: Đang xử lý
Đơn hàng C-3: Từ chối
4. Đặc điểm quan trọng và lỗi thường gặp
Khi dùng Zip, nhớ: collection kết quả sẽ có độ dài bằng collection ngắn nhất.
Ví dụ, bạn có 10 sinh viên nhưng chỉ 7 điểm, kết quả chỉ có 7 phần tử. Đây là lý do thường bị "mất" dữ liệu bất ngờ.
Lỗi #1: độ dài hoặc thứ tự collection không khớp.
Nếu bạn lỡ đảo thứ tự list hoặc quên sort trước cho đúng thứ tự, cặp kết quả sẽ sai.
Ví dụ: list sinh viên không trùng thứ tự với list điểm — Vasya có thể nhận điểm của Petya. Giống như đi tất lệch đôi: mỗi cái vẫn đúng vị trí, nhưng cảm giác lạ lắm.
Lỗi #2: một collection bị rỗng.
Nếu một list rỗng, kết quả cũng rỗng.
Zip chỉ chạy khi cả hai sequence còn phần tử. Nếu một nguồn không có dữ liệu, bạn nhận được "không gì cả".
5. Zip với nhiều collection
Ban đầu LINQ chỉ hỗ trợ kết hợp hai collection. Nhưng từ .NET 6, có overload cho phép zip BA (wow!) collection cùng lúc.
Ví dụ với ba collection:
string[] names = { "Rey", "Luke", "Leia" };
string[] planets = { "Tatooine", "Tatooine", "Alderaan" };
int[] ages = { 19, 23, 19 };
// Trong .NET 6+ có overload:
var characters = names.Zip(planets, ages, (name, planet, age) =>
$"{name} ({planet}), {age} tuổi");
foreach (var c in characters)
Console.WriteLine(c);
Rey (Tatooine), 19 tuổi
Luke (Tatooine), 23 tuổi
Leia (Alderaan), 19 tuổi
Số phần tử kết quả — theo list ngắn nhất! Nếu ages thiếu một phần tử, Luke cuối cùng sẽ "không có tuổi".
6. Zip và LINQ Query Syntax: có tồn tại không?
Bạn có thể để ý là mình luôn dùng "method syntax". Trong LINQ Query Syntax không có từ riêng cho Zip, và không thể làm qua from ... in .... Lý do: "zip" là kết hợp theo vị trí, còn LINQ query thường dùng join, select... để "liên kết" theo key, không phải theo vị trí.
Kết luận: Với Zip chỉ nên dùng method syntax:
var zipped = collection1.Zip(collection2, (a, b) => ...);
Nếu rất muốn dùng query syntax — tốt nhất đừng. Còn nếu cực kỳ muốn, bạn có thể tự viết extension method cho mình. 🙂
7. Ứng dụng Zip trong thực tế
Ví dụ thực tế: so sánh giá cũ và mới
Giả sử trong app học tập có list sản phẩm và thỉnh thoảng có giá mới. Cần in ra, giá từng sản phẩm thay đổi thế nào:
string[] productNames = { "Táo", "Chuối", "Dứa" };
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}: trước là {oldPrice}, giờ là {newPrice}, thay đổi: {newPrice - oldPrice:+#;-#;0}");
foreach (var s in priceChanges)
Console.WriteLine(s);
Táo: trước là 80, giờ là 75, thay đổi: -5
Chuối: trước là 30, giờ là 33, thay đổi: +3
Dứa: trước là 110, giờ là 120, thay đổi: +10
Thực hành: ghép kết quả từ nhiều nguồn
Trong dự án thực tế, Zip thường dùng để ghép kết quả từ nhiều API hoặc bảng, khi thứ tự dữ liệu được đảm bảo từ bên ngoài (ví dụ: mảng dự báo thời tiết và số đo thực tế).
8. Zip trong chuỗi LINQ: kết hợp với operator khác
Thường Zip không dùng riêng lẻ mà nằm trong chuỗi LINQ dài. Ví dụ, có thể filter list trước, rồi zip, sau đó tính thống kê.
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($"Điểm trung bình tổng của người qua môn: {avgScore}");
Lưu ý: nếu sau Where một list ngắn hơn, Zip sẽ cắt kết quả theo list ngắn nhất.
So sánh Zip và các method kết hợp khác
| Method | Bản chất | Kết hợp theo… | Kết quả dài bằng… |
|---|---|---|---|
|
Kết hợp phần tử theo index | Index (vị trí) | Collection ngắn nhất |
|
Kết hợp theo key | Key chung | Tất cả cặp trùng key |
|
Mỗi group key — một collection | Key | Theo collection đầu |
|
Nối collection lại với nhau | Chỉ nối vào cuối | Tổng độ dài |
Zip — là về ghép theo vị trí. Join — là ghép theo giá trị chung (ví dụ: code hoặc id).
9. Lời khuyên thực tế và lỗi điển hình
Khi dùng Zip trong app của bạn, quan trọng nhất là:
- Thứ tự phần tử ở hai collection phải khớp: nếu không sẽ "ghép" Vasya với điểm của Petya.
- Collection phải được sort hoặc chuẩn bị sẵn cho duyệt đồng bộ.
- Kết quả luôn có độ dài bằng collection ngắn nhất.
- Đừng dùng Zip nếu cần ghép dữ liệu theo key! Dùng Join cho việc đó.
- Một lỗi nữa — quên rằng một collection có duplicate, collection kia thì không. Hoặc thay đổi thứ tự khi filter và làm mất sự tương ứng.
GO TO FULL VERSION