¿Para qué sirve el método de extensión Enumerable.Zip en Linq?

Respuestas:

191

El operador Zip combina los elementos correspondientes de dos secuencias utilizando una función de selector 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
santosh singh
fuente
41
Me gusta esta respuesta porque muestra lo que sucede cuando el número de elementos no coincide, similar a la documentación de
msdn
2
¿Qué pasa si quiero que zip continúe donde una lista se queda sin elementos? en cuyo caso el elemento de la lista más corta debe tomar el valor predeterminado. La salida en este caso será A1, B2, C3, D0, E0.
liang
2
@liang Dos opciones: A) Escribe tu propia Zipalternativa. B) Escribir un método para yield returncada elemento de la lista más corta, y luego continuar yield returning defaultindefinidamente a partir de entonces. (La opción B requiere que sepa de antemano qué lista es más corta.)
jpaugh
105

Zipes para combinar dos secuencias en una. Por ejemplo, si tienes las secuencias

1, 2, 3

y

10, 20, 30

y desea obtener la secuencia que es el resultado de multiplicar elementos en la misma posición en cada secuencia para obtener

10, 40, 90

tu puedes decir

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

Se llama "zip" porque piensas en una secuencia como el lado izquierdo de una cremallera, y la otra secuencia como el lado derecho de la cremallera, y el operador de la cremallera unirá los dos lados para emparejar los dientes (el elementos de la secuencia) adecuadamente.

jason
fuente
8
Definitivamente la mejor explicación aquí.
Maxim Gershkovich
2
Me encantó el ejemplo de la cremallera. Fue muy natural. Mi impresión inicial fue si tiene algo que ver con la velocidad o algo así, como si cruzaras una calle en tu auto.
RBT
23

Se itera a través de dos secuencias y combina sus elementos, uno por uno, en una sola secuencia nueva. Entonces toma un elemento de secuencia A, lo transforma con el elemento correspondiente de la secuencia B, y el resultado forma un elemento de secuencia C.

Una forma de pensarlo es que es similar a Select, excepto que en lugar de transformar elementos de una sola colección, funciona en dos colecciones a la vez.

Del artículo de MSDN sobre el 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

Si hicieras esto en un código imperativo, probablemente harías algo como esto:

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

O si LINQ no lo tuviera Zip, podría hacer esto:

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

Esto es útil cuando tiene datos distribuidos en listas simples, en forma de matriz, cada una con la misma longitud y orden, y cada una describe una propiedad diferente del mismo conjunto de objetos. Ziple ayuda a unir esos datos en una estructura más coherente.

Entonces, si tiene una matriz de nombres de estado y otra matriz de sus abreviaturas, podría clasificarlas en una Stateclase como esta:

IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
    return stateNames.Zip(statePopulations, 
                          (name, population) => new State()
                          {
                              Name = name,
                              Population = population
                          });
}
Justin Morgan
fuente
También me gustó esta respuesta, porque menciona la similitud conSelect
iliketocode
17

NO dejes que el nombre Zipte desanime. No tiene nada que ver con comprimir, como comprimir un archivo o una carpeta (comprimir). En realidad recibe su nombre de cómo funciona una cremallera en la ropa: la cremallera en la ropa tiene 2 lados y cada lado tiene un montón de dientes. Cuando va en una dirección, la cremallera enumera (recorre) ambos lados y cierra la cremallera apretando los dientes. Cuando vas en la otra dirección, se abren los dientes. Usted termina con una cremallera abierta o cerrada.

Es la misma idea con el Zipmétodo. Considere un ejemplo donde tenemos dos colecciones. Uno tiene letras y el otro tiene el nombre de un alimento que comienza con esa letra. Por razones de claridad, los estoy llamando leftSideOfZippery rightSideOfZipper. Aquí está el código.

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

Nuestra tarea es producir una colección que tenga la letra de la fruta separada por ay :su nombre. Me gusta esto:

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

Zipal rescate. Para mantenernos al día con nuestra terminología de cremallera, llamaremos a este resultado closedZippery a los elementos de la cremallera izquierda que llamaremos leftToothy al lado derecho que llamaremos righToothpor razones obvias:

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

En lo anterior enumeramos (viajando) el lado izquierdo de la cremallera y el lado derecho de la cremallera y realizamos una operación en cada diente. La operación que estamos realizando es concatenar el diente izquierdo (letra del alimento) con ay :luego el diente derecho (nombre del alimento). Hacemos eso usando este código:

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

El resultado final es este:

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

¿Qué pasó con la última letra E?

Si está enumerando (tirando) una cremallera de ropa real y un lado, no importa el lado izquierdo o el derecho, tiene menos dientes que el otro lado, ¿qué sucederá? Bueno, la cremallera se detendrá allí. El Zipmétodo hará exactamente lo mismo: se detendrá una vez que haya alcanzado el último elemento a cada lado. En nuestro caso, el lado derecho tiene menos dientes (nombres de alimentos), por lo que se detendrá en "Donut".

CodificaciónYoshi
fuente
1
+1. Sí, el nombre "Zip" puede ser confuso al principio. Quizás "Intercalar" o "Tejer" habrían sido nombres más descriptivos para el método.
BACON
1
@bacon sí, pero entonces no habría podido usar mi ejemplo de cremallera;) Creo que una vez que descubras que es como una cremallera, es bastante sencillo después.
CodificaciónYoshi
Aunque sabía exactamente qué hace el método de extensión Zip, siempre he tenido curiosidad de por qué se llama así. En la jerga general del software, zip siempre ha significado algo más. Gran analogía :-) Debes haber leído la mente del creador.
Raghu Reddy Muttana
7

No tengo los puntos de representante para publicar en la sección de comentarios, pero para responder la pregunta relacionada:

¿Qué sucede si deseo que zip continúe donde una lista se quede sin elementos? En cuyo caso, el elemento de lista más corto debería tomar el valor predeterminado. La salida en este caso será A1, B2, C3, D0, E0. - liang 19 de noviembre de 2015 a las 3:29

Lo que haría es usar Array.Resize () para rellenar la secuencia más corta con los valores predeterminados, y luego comprimirlos () juntos.

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

Salida:

A1
B2
C3
D0
E0

Tenga en cuenta que el uso de Array.Resize () tiene una advertencia : ¿ Redim Preserve en C #?

Si se desconoce qué secuencia será la más corta, se puede crear una función que lo detecte:

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

Salida de .Zip () junto a ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Volviendo a la respuesta principal de la pregunta original , otra cosa interesante que uno podría desear hacer (cuando las longitudes de las secuencias que se van a "comprimir" son diferentes) es unirlas de tal manera que el final de la lista coincide en lugar de la parte superior. Esto se puede lograr "omitiendo" el número apropiado de elementos usando .Skip ().

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

Salida:

C1
D2
E3
un invitado extraño
fuente
Cambiar el tamaño es un desperdicio, especialmente si alguna de las colecciones es grande. Lo que realmente quiere hacer es tener una enumeración que continúe después del final de la colección, llenándola con valores vacíos a pedido (sin una colección de respaldo). Puede hacerlo 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
Parece que no estoy seguro de cómo formatear con éxito el código en un comentario ...
Pagefault
7

Muchas de las respuestas aquí demuestran Zip, pero sin explicar realmente un caso de uso de la vida real que motivaría el uso de Zip.

Un patrón particularmente común que Zipes fantástico para iterar sobre pares sucesivos de cosas. Esto se hace mediante la iteración de un enumerable Xconsigo mismo, omitiendo 1 elemento: x.Zip(x.Skip(1). Ejemplo 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)

Estos pares sucesivos son útiles para encontrar las primeras diferencias entre valores. Por ejemplo, IEnumable<MouseXPosition>se pueden usar sucesivos pares de para producir IEnumerable<MouseXDelta>. Del mismo modo, los boolvalores de muestra de a buttonse pueden interpretar en eventos como NotPressed/ Clicked/ Held/ Released. Esos eventos pueden conducir llamadas a delegar métodos. Aquí hay un ejemplo:

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

Huellas dactilares:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked
Alexander - Restablece a Monica
fuente
6

Como han dicho otros, Zip le permite combinar dos colecciones para usar en otras declaraciones de Linq o un bucle foreach.

Las operaciones que solían requerir un bucle for y dos matrices ahora se pueden realizar en un bucle foreach usando un objeto anónimo.

Un ejemplo que acabo de descubrir, que es un poco tonto, pero podría ser útil si la paralelización fuera beneficiosa sería un recorrido de cola de una sola línea con efectos secundarios:

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 los elementos actuales o en cola en una cola (el último elemento se trunca por Zip). timeSegments.Skip (1) representa los elementos siguientes o de vistazo en una cola. El método Zip combina estos dos en un solo objeto anónimo con una propiedad Siguiente y Actual. Luego filtramos con Where y hacemos cambios con AsParallel (). ForAll. Por supuesto, el último bit podría ser un foreach regular u otra instrucción Select que devuelva los segmentos de tiempo ofensivos.

Novaterata
fuente
3

El método Zip le permite "fusionar" dos secuencias no relacionadas, utilizando un proveedor de funciones de fusión por usted, la persona que llama. El ejemplo en MSDN es bastante bueno para demostrar lo que puede hacer con Zip. En este ejemplo, toma dos secuencias arbitrarias no relacionadas y las combina usando una función arbitraria (en este caso, concatenando elementos de ambas secuencias en una sola cadena).

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
Andy White
fuente
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
fuente