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.
  • Еще одна частая ошибка — забыли, что у вас в одной коллекции дубликаты, а в другой их нет. Или изменили порядок элементов на этапе фильтрации и потеряли соответствие.
2
Задача
C# SELF, 33 уровень, 4 лекция
Недоступна
Список продуктов с ценами
Список продуктов с ценами
1
Опрос
Объединение коллекций, 33 уровень, 4 лекция
Недоступен
Объединение коллекций
Продвинутые LINQ: объединения и проекции
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Slevin Уровень 59
12 февраля 2026
Забыли сказать, что если Зиповать два разных списка, а один из них короче - то результат будет обрезан по короткому. Как могли забыть упомянуть такой важный момент, я хз. А вообще ZIP в C# помощнее, чем питоновский...