Qual è l'uso del metodo di estensione Enumerable.Zip in Linq?


Risposte:


191

L'operatore Zip unisce gli elementi corrispondenti di due sequenze utilizzando una funzione di selezione specificata.

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
Mi piace questa risposta perché mostra cosa succede quando il numero di elementi non coincide, simile alla documentazione di
msdn

2
cosa succede se voglio che zip continui laddove un elenco si esaurisce di elementi? nel qual caso l'elemento dell'elenco più breve dovrebbe assumere il valore predefinito. L'output in questo caso deve essere A1, B2, C3, D0, E0.
liang

2
@liang Due scelte: A) Scrivi la tua Zipalternativa. B) Scrivi un metodo per yield returnogni elemento della lista più corta, e poi continuare yield returning defaulttempo indeterminato in seguito. (L'opzione B richiede di sapere in anticipo quale elenco è più breve.)
jpaugh

105

Zipserve per combinare due sequenze in una. Ad esempio, se hai le sequenze

1, 2, 3

e

10, 20, 30

e si desidera ottenere la sequenza risultante dalla moltiplicazione di elementi nella stessa posizione in ciascuna sequenza

10, 40, 90

potresti dire

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

Si chiama "zip" perché si considera una sequenza come il lato sinistro di una cerniera e l'altra sequenza come il lato destro della cerniera, e l'operatore zip tirerà i due lati insieme accoppiando i denti (il elementi della sequenza) in modo appropriato.


8
Sicuramente la migliore spiegazione qui.
Maxim Gershkovich,

2
Mi è piaciuto molto l'esempio della cerniera. Era così naturale. La mia impressione iniziale è stata se avesse qualcosa a che fare con la velocità o qualcosa del genere come se attraversassi una strada della tua auto.
RBT,

23

Esegue l'iterazione attraverso due sequenze e combina i loro elementi, uno per uno, in un'unica nuova sequenza. Quindi prendi un elemento della sequenza A, lo trasformi con l'elemento corrispondente dalla sequenza B e il risultato forma un elemento della sequenza C.

Un modo di pensarci è che è simile Select, tranne che invece di trasformare elementi da una singola collezione, funziona su due raccolte contemporaneamente.

Dalla articolo di MSDN sul metodo :

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 dovessi farlo con un codice imperativo, probabilmente faresti qualcosa del genere:

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

O se LINQ non avesse Zipin esso, potresti farlo:

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

Ciò è utile quando i dati sono distribuiti in elenchi semplici, simili a array, ciascuno con la stessa lunghezza e ordine e ciascuno che descrive una diversa proprietà dello stesso insieme di oggetti. Zipti aiuta a riunire quei dati insieme in una struttura più coerente.

Quindi, se hai una matrice di nomi di stato e un'altra matrice delle loro abbreviazioni, puoi raggrupparle in una Stateclasse in questo modo:

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

Mi è piaciuta anche questa risposta, perché menziona la somiglianza conSelect
iliketocode

17

NON lasciare che il nome Zipti butti via. Non ha nulla a che fare con zippare come nel comprimere un file o una cartella (compressione). In realtà prende il nome da come funziona una cerniera sui vestiti: la cerniera sui vestiti ha 2 lati e ogni lato ha un mucchio di denti. Quando si procede in una direzione, la cerniera enumera (viaggia) entrambi i lati e chiude la cerniera serrando i denti. Quando vai nella direzione opposta apre i denti. Si termina con una cerniera aperta o chiusa.

È la stessa idea con il Zipmetodo. Considera un esempio in cui abbiamo due raccolte. Uno contiene lettere e l'altro contiene il nome di un alimento che inizia con quella lettera. Per motivi di chiarezza, li chiamo leftSideOfZippere rightSideOfZipper. Ecco il codice

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

Il nostro compito è quello di produrre una raccolta che abbia la lettera del frutto separata da un :e il suo nome. Come questo:

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

ZipAl salvataggio. Per tenere il passo con la nostra terminologia con cerniera chiameremo questo risultato closedZippere gli elementi della cerniera sinistra che chiameremo leftToothe il lato destro che chiameremo righToothper ovvi motivi:

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

In quanto sopra stiamo enumerando (viaggiando) il lato sinistro della cerniera e il lato destro della cerniera ed eseguendo un'operazione su ciascun dente. L'operazione che stiamo eseguendo sta concatenando il dente sinistro (lettera dell'alimento) con un :e poi il dente destro (nome dell'alimento). Lo facciamo usando questo codice:

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

Il risultato finale è questo:

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

Cosa è successo all'ultima lettera E?

Se stai enumerando (tirando) una vera cerniera per abiti e un lato, non importa il lato sinistro o il lato destro, ha meno denti rispetto all'altro lato, cosa accadrà? Bene la cerniera si fermerà qui. Il Zipmetodo farà esattamente lo stesso: si fermerà quando avrà raggiunto l'ultimo elemento su entrambi i lati. Nel nostro caso il lato destro ha meno denti (nomi di alimenti) quindi si fermerà a "Ciambella".


1
+1. Sì, all'inizio il nome "Zip" può essere fonte di confusione. Forse "Interleave" o "Weave" sarebbero stati nomi più descrittivi per il metodo.
BACON,

1
@bacon sì, ma non avrei potuto usare il mio esempio di cerniera;) Penso che una volta che hai capito che è come una cerniera, è piuttosto semplice in seguito.
Coding Yoshi,

Sebbene sapessi esattamente cosa fa il metodo di estensione Zip, sono sempre stato curioso di sapere perché è stato chiamato così. Nel gergo generale del software zip ha sempre significato qualcos'altro. Grande analogia :-) Devi aver letto la mente del creatore.
Raghu Reddy Muttana,

7

Non ho i punti rappresentante da pubblicare nella sezione commenti, ma per rispondere alla domanda correlata:

Cosa succede se voglio che zip continui laddove un elenco si esaurisce di elementi? Nel qual caso l'elemento dell'elenco più breve dovrebbe assumere il valore predefinito. L'output in questo caso deve essere A1, B2, C3, D0, E0. - liang 19 novembre 15 alle 3:29

Quello che dovresti fare è usare Array.Resize () per riempire la sequenza più breve con i valori predefiniti e quindi comprimerli () insieme.

Esempio di codice:

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

Produzione:

A1
B2
C3
D0
E0

Si noti che l'utilizzo di Array.Resize () ha un avvertimento : Redim Preserve in C #?

Se non si conosce quale sequenza sarà la più breve, è possibile creare una funzione che la sussidi:

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

Uscita di semplice .Zip () insieme a ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Tornando alla risposta principale della domanda originale , un'altra cosa interessante che si potrebbe desiderare di fare (quando le lunghezze delle sequenze da "zippare" sono diverse) è unirle in modo tale che la fine dell'elenco partite al posto della cima. Ciò può essere ottenuto "saltando" il numero appropriato di elementi usando .Skip ().

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

Produzione:

C1
D2
E3

Il ridimensionamento è dispendioso, soprattutto se una delle raccolte è grande. Quello che vuoi davvero fare è avere un elenco che continua dopo la fine della raccolta, riempiendolo con valori vuoti su richiesta (senza una raccolta di supporto). Puoi farlo con: 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

Sembra che non sia sicuro di come formattare correttamente il codice in un commento ...
Pagefault

7

Molte delle risposte qui dimostrano Zip, ma senza spiegare davvero un caso d'uso reale che ne motiverebbe l'uso Zip.

Un modello particolarmente comune che Zipè fantastico per iterare su coppie successive di cose. Questo è fatto da un iterazione enumerabile Xcon se stesso, saltare 1 elemento: x.Zip(x.Skip(1). Esempio visivo:

 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)

Queste coppie successive sono utili per trovare le prime differenze tra i valori. Ad esempio, è IEnumable<MouseXPosition>possibile utilizzare coppie successive di per produrre IEnumerable<MouseXDelta>. Allo stesso modo, i boolvalori campionati di a buttonpossono essere interpretati in eventi come NotPressed/ Clicked/ Held/ Released. Tali eventi possono quindi indirizzare le chiamate a delegare metodi. Ecco un esempio:

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

stampe:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked

6

Come altri hanno già affermato, Zip consente di combinare due raccolte da utilizzare in ulteriori istruzioni Linq o in un ciclo foreach.

Le operazioni che richiedevano un ciclo for e due array ora possono essere eseguite in un ciclo foreach utilizzando un oggetto anonimo.

Un esempio che ho appena scoperto, è un po 'sciocco, ma potrebbe essere utile se la parallelizzazione fosse vantaggiosa sarebbe un attraversamento della coda a riga singola con effetti collaterali:

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 rappresenta gli elementi correnti o dequeued in una coda (l'ultimo elemento viene troncato da Zip). timeSegments.Skip (1) rappresenta gli elementi successivi o sbirciati in una coda. Il metodo Zip combina questi due in un unico oggetto anonimo con una proprietà Next e Current. Quindi filtriamo con Where e apportiamo modifiche con AsParallel (). ForAll. Naturalmente l'ultimo bit potrebbe essere solo una foreach regolare o un'altra istruzione Select che restituisce i segmenti temporali offensivi.


3

Il metodo Zip consente di "unire" due sequenze non correlate, utilizzando un provider di funzioni di fusione, il chiamante. L'esempio su MSDN è in realtà abbastanza buono nel dimostrare cosa puoi fare con Zip. In questo esempio, prendi due sequenze arbitrarie non correlate e le combini usando una funzione arbitraria (in questo caso, concatenando gli elementi di entrambe le sequenze in un'unica stringa).

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.