JavaRush /Курси /C# SELF /Об’єднання за позицією з Z...

Об’єднання за позицією з Zip

C# SELF
Рівень 33 , Лекція 4
Відкрита

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
З’єднує за ключем Загальний ключ Усі пари, що збіглися
GroupJoin
Для кожного ключа — колекція Ключ Першої колекції
Concat
Склеює колекції одна за одною Просто в кінець Сума довжин

Zip — це про позиційну відповідність. Join — про відповідність за спільним значенням (наприклад, кодом або ID).

9. Практичні поради та типові помилки

Коли використовуватимете Zip у своєму застосунку, головне — переконайтеся, що:

  • Порядок елементів в обох колекціях узгоджений: інакше «зіпнете» Василя з оцінками Петра.
  • Колекції коректно відсортовані або заздалегідь підготовлені до синхронного перебору.
  • Результат завжди буде за довжиною найкоротшої колекції.
  • Не використовуйте Zip, якщо треба зіставляти дані за якимось ключем! Для цього є Join.
  • Ще одна часта помилка — забути, що в одній колекції дублікати, а в іншій їх немає. Або змінити порядок елементів на етапі фільтрації й утратити відповідність.
1
Опитування
Об'єднання колекцій, рівень 33, лекція 4
Недоступний
Об'єднання колекцій
Просунуті LINQ: об'єднання та проєкції
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ