1. Вступ
Уявіть дві стопки листів: перша — з конвертами, на яких указані імена, друга — з листівками для цих адресатів. Ваше завдання — зіставити кожен конверт із листівкою з того ж положення у другій стопці. Перший конверт — із першою листівкою, другий — із другою і так далі. Ви йдете обома стопками, берете по одному елементу з кожної і «зіпуєте» їх разом.
У програмуванні таку операцію називають zip — за аналогією з блискавкою, яка «зчіплює» дві сторони, поєднуючи їх чітко за позиціями.
У LINQ це зручний і потужний інструмент для об’єднання даних із двох (а іноді й більше) послідовностей, коли важливий порядок: перший елемент — із першим, другий — із другим і так далі.
Важливо пам’ятати: Zip працює лише доти, доки в обох колекціях є елементи. Якщо одна з них коротша — результат буде обмежений довжиною найкоротшої.
Синтаксис і принципи роботи
Zip бере два (або більше) списки і об’єднує їх у новий: елементи з’єднуються за індексами, тобто 0-й із 0-им, 1-й із 1-им і так далі. Якщо один зі списків закінчиться раніше, нові пари більше не формуватимуться — результат буде за довжиною найкоротшого списку.
Сигнатура (основна):
IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> resultSelector
)
Зверніть увагу: Zip повертає не просто пари, а те, що ви вкажете у resultSelector. Це може бути кортеж, рядок, об’єкт чи що завгодно — усе залежить від вашої логіки.
- first і second — колекції для об’єднання.
- resultSelector — функція, яка приймає чергові елементи обох колекцій і повертає новий результат.
Простими словами: проходимо обидві колекції одночасно й на кожному кроці перетворюємо їхню пару так, як нам потрібно.
2. Приклади використання Zip — від простого до складного
Найпростіший приклад: додавання двох числових масивів
Припустимо, у нас є два списки:
- Бали з математики
- Бали з літератури
// Два масиви з балами студентів
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 і колекція студентів. А тепер — окремий масив рейтингів, зіставлений за порядком:
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 елементів. Це часта причина неочікуваних «втрат» даних.
Помилка № 1: неузгоджені довжини або порядок колекцій.
Якщо ви випадково переплутали порядок списків або забули заздалегідь відсортувати їх так, щоб елементи відповідали один одному, отримаєте некоректні пари.
Уявіть: список студентів не збігається з порядком оцінок — Василь може отримати оцінку Петра. Це як узути шкарпетки з різних пар: кожна ніби на місці, але відчуття дивне.
Помилка № 2: одна з колекцій порожня.
Якщо хоча б один список порожній, результат буде порожнім.
Zip працює лише доти, доки є елементи в обох послідовностях. Якщо одне з джерел не містить даних, ви отримаєте «нічого».
5. Zip з кількома колекціями
Спочатку LINQ підтримував лише об’єднання двох колекцій. Але починаючи з .NET 6, з’явилося перевантаження, яке дає змогу поєднувати три колекції одночасно.
Приклад із трьома колекціями:
string[] names = { "Рей", "Люк", "Лея" };
string[] planets = { "Татуїн", "Татуїн", "Альдераан" };
int[] ages = { 19, 23, 19 };
// У .NET 6+ з’явилося перевантаження:
var characters = names.Zip(planets, ages, (name, planet, age) =>
$"{name} ({planet}), {age} років");
foreach (var c in characters)
Console.WriteLine(c);
Рей (Татуїн), 19 років
Люк (Татуїн), 23 років
Лея (Альдераан), 19 років
Кількість елементів у результаті визначається найкоротшим списком. Якщо ages буде на один менше, останній Люк піде «без віку».
6. Zip і LINQ Query Syntax: чи існує?
Ви могли помітити, що ми весь час використовуємо синтаксис методів (Method Syntax). У LINQ Query Syntax окремого слова для Zip немає, і зробити це через «from … in …» не можна. Причина проста: «зіпування» — це парне об’єднання за позицією, тоді як запити в LINQ зазвичай комбінують join, select тощо для зв’язування за ключем, а не за позицією.
Підсумок: для Zip використовуємо винятково синтаксис методів:
var zipped = collection1.Zip(collection2, (a, b) => ...);
Якщо дуже хочеться застосувати query-синтаксис — краще утримайтеся. А якщо таки наполягаєте, можна написати власний метод-розширення.
7. Застосування Zip у реальних застосунках
Приклад із вашої практики: порівняння старих і нових цін
Припустімо, у нашому навчальному застосунку є список продуктів, і до них періодично приходять нові ціни. Треба вивести, як змінилася ціна кожного товару:
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: комбінуємо з іншими операторами
Часто Zip використовують не окремо, а у складі довгих LINQ-ланцюжків. Наприклад, можна спочатку відфільтрувати списки, потім їх «зіпнути», а далі — обчислити підсумкову статистику.
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 один зі списків став коротшим, Zip обмежить результат найкоротшим.
Порівняння Zip та інших методів об’єднання
| Метод | Суть | З’єднує за… | Результат за довжиною… |
|---|---|---|---|
|
З’єднує елементи за індексом | Індекс (позиція) | Найкоротшої колекції |
|
З’єднує за ключем | Загальний ключ | Усі пари, що збіглися |
|
Для кожного ключа — колекція | Ключ | Першої колекції |
|
Склеює колекції одна за одною | Просто в кінець | Сума довжин |
Zip — це про позиційну відповідність. Join — про відповідність за спільним значенням (наприклад, кодом або ID).
9. Практичні поради та типові помилки
Коли використовуватимете Zip у своєму застосунку, головне — переконайтеся, що:
- Порядок елементів в обох колекціях узгоджений: інакше «зіпнете» Василя з оцінками Петра.
- Колекції коректно відсортовані або заздалегідь підготовлені до синхронного перебору.
- Результат завжди буде за довжиною найкоротшої колекції.
- Не використовуйте Zip, якщо треба зіставляти дані за якимось ключем! Для цього є Join.
- Ще одна часта помилка — забути, що в одній колекції дублікати, а в іншій їх немає. Або змінити порядок елементів на етапі фільтрації й утратити відповідність.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ