Какая польза от метода расширения Enumerable.Zip в Linq?

131

Какая польза от Enumerable.Zipметода расширения в Linq?

user634871
источник
2
Вы имеете в виду это: msdn.microsoft.com/en-us/library/dd267698.aspx ? - Что вы пытаетесь достичь?
13
Это похоже на соединение двух сторон молнии.
Кевин Панко
Возможный дубликат. Какова цель функции zip (как в Python или C # 4.0)?
Джулио Каччин

Ответы:

192

Оператор Zip объединяет соответствующие элементы двух последовательностей, используя указанную функцию-селектор.

var letters= new string[] { "A", "B", "C", "D", "E" };
var numbers= new int[] { 1, 2, 3 };
var q = letters.Zip(numbers, (l, n) => l + n.ToString());
foreach (var s in q)
    Console.WriteLine(s);

Ouput

A1
B2
C3
Сантош Сингх
источник
42
Мне нравится этот ответ, потому что он показывает, что происходит, когда количество элементов не совпадает, аналогично документации
msdn
2
что, если я хочу, чтобы zip продолжался там, где в одном списке заканчиваются элементы? в этом случае более короткий элемент списка должен принимать значение по умолчанию. В этом случае выведите A1, B2, C3, D0, E0.
лян
2
@liang Два варианта: A) Напишите свой собственный Zipвариант. Б) Написать метод yield returnкаждому элементу короткий список, а затем продолжить yield returnИНГ на defaultнеопределенный срок после этого. (Вариант B требует, чтобы вы знали заранее, какой список короче.)
jpaugh
105

Zipпредназначен для объединения двух последовательностей в одну. Например, если у вас есть последовательности

1, 2, 3

и

10, 20, 30

и вы хотите, чтобы последовательность, являющаяся результатом умножения элементов в одной позиции в каждой последовательности, получила

10, 40, 90

ты мог бы сказать

var left = new[] { 1, 2, 3 };
var right = new[] { 10, 20, 30 };
var products = left.Zip(right, (m, n) => m * n);

Это называется «застежка-молния», потому что вы думаете об одной последовательности как о левой стороне застежки-молнии, а о другой последовательности как о правой стороне застежки-молнии, и оператор застежки-молнии стягивает две стороны вместе, создавая пары зубцов ( элементы последовательности) соответственно.

Ясон
источник
8
Определенно лучшее объяснение здесь.
Максим Гершкович
2
Очень понравился пример молнии. Это было так естественно. Мое первое впечатление было, связано ли это со скоростью или чем-то в этом роде, как будто вы мчитесь по улице на своей машине.
RBT
23

Он выполняет итерацию по двум последовательностям и объединяет их элементы один за другим в одну новую последовательность. Итак, вы берете элемент последовательности A, преобразуете его соответствующим элементом из последовательности B, и результат формирует элемент последовательности C.

Один из способов думать об этом - это то, что он похож на Select, за исключением того, что вместо преобразования элементов из одной коллекции он работает с двумя коллекциями одновременно.

Из статьи MSDN о методе :

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);

foreach (var item in numbersAndWords)
    Console.WriteLine(item);

// This code produces the following output:

// 1 one
// 2 two
// 3 three

Если бы вы сделали это в императивном коде, вы, вероятно, сделали бы что-то вроде этого:

for (int i = 0; i < numbers.Length && i < words.Length; i++)
{
    numbersAndWords.Add(numbers[i] + " " + words[i]);
}

Или, если LINQ не был Zipв нем, вы могли бы сделать это:

var numbersAndWords = numbers.Select(
                          (num, i) => num + " " + words[i]
                      );

Это полезно, когда данные разбиты на простые списки, похожие на массивы, каждый с одинаковой длиной и порядком, и каждый описывает разные свойства одного и того же набора объектов. Zipпомогает объединить эти фрагменты данных в более согласованную структуру.

Итак, если у вас есть массив имен состояний и другой массив их сокращений, вы можете сопоставить их в Stateкласс следующим образом:

IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
    return stateNames.Zip(statePopulations, 
                          (name, population) => new State()
                          {
                              Name = name,
                              Population = population
                          });
}
Джастин Морган
источник
Мне тоже понравился этот ответ, потому что в нем упоминается сходство сSelect
iliketocode
17

НЕ позволяйте имени Zipсбивать вас с толку. Это не имеет ничего общего с архивированием, как с архивированием файла или папки (сжатием). На самом деле он получил свое название от того, как работает застежка-молния на одежде: молния на одежде имеет 2 стороны, и каждая сторона имеет несколько зубцов. Когда вы идете в одном направлении, молния пронумеровывает (перемещается) с обеих сторон и закрывает молнию, стиснув зубы. Когда вы идете в другую сторону, зубы открываются. У вас либо открытая, либо закрытая молния.

То же самое и с Zipметодом. Рассмотрим пример, когда у нас есть две коллекции. Один содержит буквы, а другой - название продукта, которое начинается с этой буквы. Для наглядности я их называю leftSideOfZipperи rightSideOfZipper. Вот код.

var leftSideOfZipper = new List<string> { "A", "B", "C", "D", "E" };
var rightSideOfZipper = new List<string> { "Apple", "Banana", "Coconut", "Donut" };

Наша задача - создать одну коллекцию, в которой буква фруктов разделена буквой «А» :и ее название. Как это:

A : Apple
B : Banana
C : Coconut
D : Donut

Zipдля спасения. Чтобы не отставать от нашей терминологии застежки-молнии, мы будем называть этот результат closedZipperи элементы левой молнии, которую мы будем называть, leftToothи правой стороны, которые мы будем называть righToothпо очевидным причинам:

var closedZipper = leftSideOfZipper
   .Zip(rightSideOfZipper, (leftTooth, rightTooth) => leftTooth + " : " + rightTooth).ToList();

Выше мы перечисляем (перемещаемся) левую сторону молнии и правую сторону молнии и выполняем операцию с каждым зубом. Операция, которую мы выполняем, соединяет левый зуб (буква еды) с буквой a, :а затем правый зуб (название продукта). Мы делаем это с помощью этого кода:

(leftTooth, rightTooth) => leftTooth + " : " + rightTooth)

Конечный результат таков:

A : Apple
B : Banana
C : Coconut
D : Donut

Что случилось с последней буквой Е?

Что произойдет, если вы перечисляете (натягиваете) настоящую молнию на одежде, и у одной стороны, неважно с левой или с правой стороны, меньше зубцов, чем с другой стороны? На этом молния остановится. ZipМетод будет делать точно так же: Он остановится , как только он достиг последнего пункта с обеих сторон. В нашем случае на правой стороне меньше зубов (названий продуктов), поэтому она остановится на «Пончике».

CodingYoshi
источник
1
+1. Да, название «Zip» поначалу может сбивать с толку. Возможно, «Interleave» или «Weave» были бы более описательными названиями для метода.
BACON
1
@bacon, да, но тогда я бы не смог использовать свой пример с застежкой-молнией;) Думаю, как только вы поймете, что это как застежка-молния, потом все будет довольно просто.
CodingYoshi
Хотя я точно знал, что делает метод расширения Zip, мне всегда было любопытно, почему он так назван. На общем программном жаргоне zip всегда означал что-то другое. Отличная аналогия :-) Вы, должно быть, прочитали мысли создателя.
Рагху Редди Муттана
7

У меня нет репутации для публикации в разделе комментариев, но я хочу ответить на связанный вопрос:

Что, если я хочу, чтобы zip продолжался там, где в одном списке заканчиваются элементы? В этом случае более короткий элемент списка должен принимать значение по умолчанию. В этом случае выведите A1, B2, C3, D0, E0. - liang 19 нояб.

Что бы вы сделали, так это использовать Array.Resize (), чтобы дополнить более короткую последовательность значениями по умолчанию, а затем Zip () их вместе.

Пример кода:

var letters = new string[] { "A", "B", "C", "D", "E" };
var numbers = new int[] { 1, 2, 3 };
if (numbers.Length < letters.Length)
    Array.Resize(ref numbers, letters.Length);
var q = letters.Zip(numbers, (l, n) => l + n.ToString());
foreach (var s in q)
    Console.WriteLine(s);

Вывод:

A1
B2
C3
D0
E0

Обратите внимание, что при использовании Array.Resize () есть предостережение : Redim Preserve в C #?

Если неизвестно, какая последовательность будет короче, можно создать функцию, которая ее обрабатывает:

static void Main(string[] args)
{
    var letters = new string[] { "A", "B", "C", "D", "E" };
    var numbers = new int[] { 1, 2, 3 };
    var q = letters.Zip(numbers, (l, n) => l + n.ToString()).ToArray();
    var qDef = ZipDefault(letters, numbers);
    Array.Resize(ref q, qDef.Count());
    // Note: using a second .Zip() to show the results side-by-side
    foreach (var s in q.Zip(qDef, (a, b) => string.Format("{0, 2} {1, 2}", a, b)))
        Console.WriteLine(s);
}

static IEnumerable<string> ZipDefault(string[] letters, int[] numbers)
{
    switch (letters.Length.CompareTo(numbers.Length))
    {
        case -1: Array.Resize(ref letters, numbers.Length); break;
        case 0: goto default;
        case 1: Array.Resize(ref numbers, letters.Length); break;
        default: break;
    }
    return letters.Zip(numbers, (l, n) => l + n.ToString()); 
}

Вывод простого .Zip () вместе с ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Возвращаясь к основному ответу на исходный вопрос , еще одна интересная вещь, которую можно было бы сделать (когда длины «заархивируемых» последовательностей различаются), - это соединить их таким образом, чтобы конец списка совпадения вместо вершины. Этого можно добиться, «пропустив» соответствующее количество элементов с помощью .Skip ().

foreach (var s in letters.Skip(letters.Length - numbers.Length).Zip(numbers, (l, n) => l + n.ToString()).ToArray())
Console.WriteLine(s);

Вывод:

C1
D2
E3
странный гость
источник
Изменение размера расточительно, особенно если какая-либо из коллекций велика. Что вам действительно нужно, так это иметь перечисление, которое продолжается после конца коллекции, заполняя его пустыми значениями по запросу (без вспомогательной коллекции). Вы можете сделать это с помощью: public static IEnumerable<T> Pad<T>(this IEnumerable<T> input, long minLength, T value = default(T)) { long numYielded = 0; foreach (T element in input) { yield return element; ++numYielded; } while (numYielded < minLength) { yield return value; ++numYielded; } }
Pagefault
Кажется, я не уверен, как правильно форматировать код в комментарии ...
Pagefault
7

Многие ответы здесь демонстрируют Zip, но без реального объяснения реального варианта использования, который мотивировал бы использование Zip.

Один из наиболее распространенных паттернов, который Zipотлично подходит для перебора последовательных пар вещей. Это делается итерируя Перечислимый Xс собой, пропустив 1 элемент: x.Zip(x.Skip(1). Визуальный пример:

 x | x.Skip(1) | x.Zip(x.Skip(1), ...)
---+-----------+----------------------
   |    1      |
 1 |    2      | (1, 2)
 2 |    3      | (2, 1)
 3 |    4      | (3, 2)
 4 |    5      | (4, 3)

Эти последовательные пары полезны для поиска первых различий между значениями. Например, IEnumable<MouseXPosition>для производства могут использоваться следующие друг за другом пары IEnumerable<MouseXDelta>. Точно так же выборочные boolзначения a buttonможно интерпретировать в такие события, как NotPressed/ Clicked/ Held/ Released. Затем эти события могут вызывать вызовы методов делегирования. Вот пример:

using System;
using System.Collections.Generic;
using System.Linq;

enum MouseEvent { NotPressed, Clicked, Held, Released }

public class Program {
    public static void Main() {
        // Example: Sampling the boolean state of a mouse button
        List<bool> mouseStates = new List<bool> { false, false, false, false, true, true, true, false, true, false, false, true };

        mouseStates.Zip(mouseStates.Skip(1), (oldMouseState, newMouseState) => {
            if (oldMouseState) {
                if (newMouseState) return MouseEvent.Held;
                else return MouseEvent.Released;
            } else {
                if (newMouseState) return MouseEvent.Clicked;
                else return MouseEvent.NotPressed;
            }
        })
        .ToList()
        .ForEach(mouseEvent => Console.WriteLine(mouseEvent) );
    }
}

Печать:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked
Александр - Восстановить Монику
источник
6

Как утверждали другие, Zip позволяет объединить две коллекции для использования в дальнейших операторах Linq или в цикле foreach.

Операции, которые раньше требовали цикла for и двух массивов, теперь могут выполняться в цикле foreach с использованием анонимного объекта.

Пример, который я только что обнаружил, выглядит глупо, но мог бы быть полезным, если бы распараллеливание было полезным, было бы обход очереди одной строки с побочными эффектами:

timeSegments
    .Zip(timeSegments.Skip(1), (Current, Next) => new {Current, Next})
    .Where(zip => zip.Current.EndTime > zip.Next.StartTime)
    .AsParallel()
    .ForAll(zip => zip.Current.EndTime = zip.Next.StartTime);

timeSegments представляет текущие или исключенные из очереди элементы в очереди (последний элемент усекается Zip). timeSegments.Skip (1) представляет следующие или просматриваемые элементы в очереди. Метод Zip объединяет эти два объекта в один анонимный объект со свойствами Next и Current. Затем мы фильтруем с помощью Where и вносим изменения с помощью AsParallel (). ForAll. Конечно, последний бит может быть просто обычным foreach или другим оператором Select, который возвращает неправильные временные сегменты.

Novaterata
источник
3

Метод Zip позволяет вам «объединить» две несвязанные последовательности, используя провайдер функции слияния, сделанный вами, вызывающей стороной. Пример на MSDN на самом деле довольно хорошо демонстрирует, что вы можете делать с Zip. В этом примере вы берете две произвольные, несвязанные последовательности и объединяете их с помощью произвольной функции (в данном случае просто объединяете элементы из обеих последовательностей в одну строку).

int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };

var numbersAndWords = numbers.Zip(words, (first, second) => first + " " + second);

foreach (var item in numbersAndWords)
    Console.WriteLine(item);

// This code produces the following output:

// 1 one
// 2 two
// 3 three
Энди Уайт
источник
0
string[] fname = { "mark", "john", "joseph" };
string[] lname = { "castro", "cruz", "lopez" };

var fullName = fname.Zip(lname, (f, l) => f + " " + l);

foreach (var item in fullName)
{
    Console.WriteLine(item);
}
// The output are

//mark castro..etc
CodeSlayer
источник