¿Cuál es el uso del Enumerable.Zip
método de extensión en Linq?
¿Cuál es el uso del Enumerable.Zip
método de extensión en Linq?
Respuestas:
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
Zip
alternativa. B) Escribir un método para yield return
cada elemento de la lista más corta, y luego continuar yield return
ing default
indefinidamente a partir de entonces. (La opción B requiere que sepa de antemano qué lista es más corta.)
Zip
es 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.
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. Zip
le 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 State
clase como esta:
IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
return stateNames.Zip(statePopulations,
(name, population) => new State()
{
Name = name,
Population = population
});
}
Select
NO dejes que el nombre Zip
te 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 Zip
mé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 leftSideOfZipper
y 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
Zip
al rescate. Para mantenernos al día con nuestra terminología de cremallera, llamaremos a este resultado closedZipper
y a los elementos de la cremallera izquierda que llamaremos leftTooth
y al lado derecho que llamaremos righTooth
por 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 Zip
mé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".
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
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; } }
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 Zip
es fantástico para iterar sobre pares sucesivos de cosas. Esto se hace mediante la iteración de un enumerable X
consigo 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 bool
valores de muestra de a button
se 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
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.
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
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