Qual é o uso do método de extensão Enumerable.Zip no Linq?


Respostas:


191

O operador Zip mescla os elementos correspondentes de duas seqüências usando uma função seletora especificada.

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

41
I como esta resposta, porque mostra o que acontece quando o número de elementos não corresponde, similar à documentação da MSDN
DLeh

2
e se eu quiser que o zip continue onde uma lista fica sem elementos? nesse caso, o elemento da lista mais curto deve assumir o valor padrão. A saída neste caso deve ser A1, B2, C3, D0, E0.
Liang

2
@liang Duas opções: A) Escreva sua própria Zipalternativa. B) Escrever um método para yield returncada elemento da lista mais curta, e, em seguida, continuar yield returning defaultpor tempo indeterminado. (Opção B requer que você sabe de antemão que a lista é mais curto.)
jpaugh

105

Zipé para combinar duas seqüências em uma. Por exemplo, se você tiver as sequências

1, 2, 3

e

10, 20, 30

e você deseja que a sequência resultante da multiplicação de elementos na mesma posição em cada sequência obtenha

10, 40, 90

você poderia dizer

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

É chamado de "zip" porque você pensa em uma sequência como o lado esquerdo de um zíper, e a outra sequência como o lado direito do zíper, e o operador de zip puxará os dois lados juntos emparelhando os dentes (o elementos da sequência) adequadamente.


8
Definitivamente a melhor explicação aqui.
Maxim Gershkovich 30/10/12

2
Adorei o exemplo de zíper. Foi tão natural. Minha impressão inicial foi se ele tem algo a ver com velocidade ou algo parecido como se você passasse por uma rua em seu carro.
RBT

23

Ele itera através de duas seqüências e combina seus elementos, um por um, em uma única nova seqüência. Então você pega um elemento da sequência A, transforma-o com o elemento correspondente da sequência B e o resultado forma um elemento da sequência C.

Uma maneira de pensar é que é semelhante a Select, exceto que, em vez de transformar itens de uma única coleção, funciona em duas coleções ao mesmo tempo.

No artigo do MSDN sobre o método :

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

Se você fizesse isso em código imperativo, provavelmente faria algo assim:

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

Ou se o LINQ não continha Zip, você poderia fazer o seguinte:

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

Isso é útil quando você tem dados espalhados em listas simples, semelhantes a matrizes, cada uma com o mesmo comprimento e ordem, e cada uma descrevendo uma propriedade diferente do mesmo conjunto de objetos. Zipajuda a reunir esses dados em uma estrutura mais coerente.

Portanto, se você tiver uma matriz de nomes de estados e outra matriz de abreviações, poderá agrupá-las em uma Stateclasse como esta:

IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
    return stateNames.Zip(statePopulations, 
                          (name, population) => new State()
                          {
                              Name = name,
                              Population = population
                          });
}

Eu gostei desta resposta também, porque menciona a semelhança comSelect
iliketocode

17

NÃO deixe o nome Zip jogá-lo fora. Não tem nada a ver com compactar, como compactar um arquivo ou uma pasta (compactar). Na verdade, ele recebe o nome de como funciona o zíper nas roupas: o zíper nas roupas tem dois lados e cada lado tem um monte de dentes. Quando você segue uma direção, o zíper enumera (viaja) dos dois lados e fecha o zíper apertando os dentes. Quando você vai na outra direção, ele abre os dentes. Você termina com um zíper aberto ou fechado.

É a mesma idéia com o Zipmétodo. Considere um exemplo em que temos duas coleções. Um contém letras e o outro contém o nome de um item alimentar que começa com essa letra. Para fins de clareza, eu os estou chamando leftSideOfZippere rightSideOfZipper. Aqui está o código.

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

Nossa tarefa é produzir uma coleção que tenha a letra da fruta separada por a :e seu nome. Como isso:

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

Zippara o resgate. Para acompanhar a terminologia do zíper, chamaremos esse resultado closedZippere os itens do zíper esquerdo, leftTootho lado direito, e o lado direito, righToothpor razões óbvias:

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

No exposto acima, estamos enumerando (deslocando) o lado esquerdo do zíper e o lado direito do zíper e realizando uma operação em cada dente. A operação que estamos realizando está concatenando o dente esquerdo (letra do alimento) com um :e depois o dente direito (nome do alimento). Fazemos isso usando este código:

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

O resultado final é este:

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

O que aconteceu com a última letra E?

Se você está enumerando (puxando) um zíper de roupa real e um lado, não importa o lado esquerdo ou o lado direito, tem menos dentes do que o outro lado, o que acontecerá? Bem, o zíper vai parar por aí. O Zipmétodo fará exatamente o mesmo: será interrompido quando atingir o último item de cada lado. No nosso caso, o lado direito tem menos dentes (nomes de alimentos) e, portanto, para em "Donut".


1
+1. Sim, o nome "Zip" pode ser confuso no início. Talvez "intercalar" ou "tecer" tivesse sido nomes mais descritivos para o método.
BACON

1
@ Bacon sim, mas então eu não teria sido capaz de usar o meu exemplo de zíper;) Eu acho que depois que você descobrir que é como um zíper, é bastante direto depois.
CodingYoshi

Embora eu soubesse exatamente o que o método de extensão Zip faz, sempre fiquei curioso para saber por que foi chamado assim. No jargão geral do software, zip sempre significou outra coisa. Ótima analogia :-) Você deve ter lido a mente do criador.
Raghu Reddy Muttana

7

Não tenho os pontos de representante para postar na seção de comentários, mas para responder à pergunta relacionada:

E se eu quiser que o zip continue onde uma lista fica sem elementos? Nesse caso, o elemento da lista mais curto deve assumir o valor padrão. A saída neste caso deve ser A1, B2, C3, D0, E0. # Liang Nov 19 '15 às 3:29

O que você faria é usar Array.Resize () para preencher a sequência mais curta com os valores padrão e depois Zip () juntos.

Exemplo de código:

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);

Resultado:

A1
B2
C3
D0
E0

Observe que o uso de Array.Resize () tem uma ressalva : Redim Preserve in C #?

Se não se sabe qual sequência será a mais curta, uma função pode ser criada que a exclua:

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()); 
}

Saída do .Zip () comum ao lado de ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Voltando à resposta principal da pergunta original , outra coisa interessante que alguém pode querer fazer (quando os comprimentos das seqüências a serem "compactadas" são diferentes) é juntá-las de tal maneira que o final da lista corresponde ao invés do topo. Isso pode ser feito "pulando" o número apropriado de itens usando .Skip ().

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

Resultado:

C1
D2
E3

O redimensionamento é um desperdício, especialmente se uma das coleções for grande. O que você realmente deseja fazer é ter uma enumeração que continue após o final da coleção, preenchendo-a com valores vazios sob demanda (sem uma coleção de backup). Você pode fazer isso com: 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 5/11/19

Parece que não tenho certeza de como formatar com êxito o código em um comentário ...
Pagefault

7

Muitas das respostas aqui demonstram Zip, mas sem realmente explicar um caso de uso da vida real que motivaria o uso Zip.

Um padrão particularmente comum que Zipé fantástico para iterar sobre pares sucessivos de coisas. Isso é feito por iteração um enumeráveis Xcom ele mesmo, pulando 1 elemento: x.Zip(x.Skip(1). Exemplo visual:

 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)

Esses pares sucessivos são úteis para encontrar as primeiras diferenças entre os valores. Por exemplo, pares sucessivos de IEnumable<MouseXPosition>podem ser usados ​​para produzir IEnumerable<MouseXDelta>. Da mesma forma, os boolvalores amostrados de a buttonpodem ser interpretados em eventos como NotPressed/ Clicked/ Held/ Released. Esses eventos podem direcionar chamadas para delegar métodos. Aqui está um exemplo:

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) );
    }
}

Impressões:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked

6

Como já foi dito, o Zip permite combinar duas coleções para uso em outras instruções do Linq ou em um loop foreach.

As operações que costumavam exigir um loop for e duas matrizes agora podem ser feitas em um loop foreach usando um objeto anônimo.

Um exemplo que acabei de descobrir, que é meio bobo, mas poderia ser útil se a paralelização fosse benéfica, seria uma travessia de fila única com efeitos colaterais:

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 representa os itens atuais ou desenfileirados em uma fila (o último elemento é truncado por Zip). timeSegments.Skip (1) representa os itens seguintes ou espiar em uma fila. O método Zip combina esses dois em um único objeto anônimo com a propriedade Next e Current. Em seguida, filtramos com Where e fazemos alterações com AsParallel (). ForAll. É claro que o último bit poderia ser apenas uma foreach regular ou outra instrução Select que retorne os segmentos de tempo ofensivos.


3

O método Zip permite "mesclar" duas seqüências não relacionadas, usando um provedor de função de mesclagem por você, o chamador. O exemplo no MSDN é realmente muito bom em demonstrar o que você pode fazer com o Zip. Neste exemplo, você pega duas seqüências arbitrárias não relacionadas e as combina usando uma função arbitrária (nesse caso, apenas concatenando itens de ambas as seqüências em uma única sequência).

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.