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.
- Еще одна частая ошибка — забыли, что у вас в одной коллекции дубликаты, а в другой их нет. Или изменили порядок элементов на этапе фильтрации и потеряли соответствие.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ