CodeGym /Các khóa học /C# SELF /Kết hợp theo vị trí với Zi...

Kết hợp theo vị trí với Zip

C# SELF
Mức độ , Bài học
Có sẵn

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.

  • firstsecond — 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
Cần tính tổng điểm cho từng sinh viên ở cùng vị trí.


// 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…
Zip
Kết hợp phần tử theo index Index (vị trí) Collection ngắn nhất
Join
Kết hợp theo key Key chung Tất cả cặp trùng key
GroupJoin
Mỗi group key — một collection Key Theo collection đầu
Concat
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.
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Kết hợp các bộ sưu tập
LINQ nâng cao: kết hợp và chiếu
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION