À quoi sert la méthode d'extension Enumerable.Zip dans Linq?

Réponses:

191

L'opérateur Zip fusionne les éléments correspondants de deux séquences à l'aide d'une fonction de sélection spécifiée.

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
la source
41
J'aime cette réponse car elle montre ce qui se passe lorsque le nombre d'éléments ne correspond pas, similaire à la documentation
msdn
2
Et si je veux que zip continue là où une liste manque d'éléments? auquel cas l'élément de liste le plus court doit prendre la valeur par défaut. Dans ce cas, la sortie doit être A1, B2, C3, D0, E0.
liang
2
@liang Deux choix: A) Écrivez votre propre Zipalternative. B) Ecrire une méthode pour yield returnchaque élément de la liste plus courte, puis continuer yield returning defaultindéfiniment par la suite. (L'option B vous oblige à savoir à l'avance quelle liste est la plus courte.)
jpaugh
105

Zipest pour combiner deux séquences en une seule. Par exemple, si vous avez les séquences

1, 2, 3

et

10, 20, 30

et vous voulez que la séquence qui résulte de la multiplication des éléments dans la même position dans chaque séquence pour obtenir

10, 40, 90

Tu pourrais dire

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

On l'appelle «zip» parce que vous pensez à une séquence comme le côté gauche d'une fermeture à glissière et l'autre séquence comme le côté droit de la fermeture à glissière, et l'opérateur de la fermeture éclair tirera les deux côtés ensemble en les appariant des dents (le éléments de la séquence) de manière appropriée.

Jason
la source
8
Certainement la meilleure explication ici.
Maxim Gershkovich
2
J'ai adoré l'exemple de la fermeture à glissière. C'était tellement naturel. Ma première impression était de savoir si cela avait quelque chose à voir avec la vitesse ou quelque chose du genre, comme si vous filiez dans une rue avec votre voiture.
RBT
23

Il parcourt deux séquences et combine leurs éléments, un par un, en une seule nouvelle séquence. Donc, vous prenez un élément de la séquence A, le transformez avec l'élément correspondant de la séquence B, et le résultat forme un élément de la séquence C.

Une façon d'y penser est que c'est similaire Select, sauf qu'au lieu de transformer des éléments d'une seule collection, cela fonctionne sur deux collections à la fois.

De l' article MSDN sur la méthode :

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 vous deviez faire cela dans un code impératif, vous feriez probablement quelque chose comme ceci:

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

Ou si LINQ n'en avait pas Zip, vous pouvez le faire:

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

Ceci est utile lorsque vous avez des données réparties dans des listes simples de type tableau, chacune avec la même longueur et le même ordre, et chacune décrivant une propriété différente du même ensemble d'objets. Zipvous aide à assembler ces éléments de données en une structure plus cohérente.

Donc, si vous avez un tableau de noms d'états et un autre tableau de leurs abréviations, vous pouvez les rassembler dans une Stateclasse comme ceci:

IEnumerable<State> GetListOfStates(string[] stateNames, int[] statePopulations)
{
    return stateNames.Zip(statePopulations, 
                          (name, population) => new State()
                          {
                              Name = name,
                              Population = population
                          });
}
Justin Morgan
la source
J'ai aimé cette réponse aussi, car elle mentionne la similitude avecSelect
iliketocode
17

NE laissez PAS le nom Zipvous décourager. Cela n'a rien à voir avec la compression comme dans la compression d'un fichier ou d'un dossier (compression). Il tire son nom du fonctionnement d'une fermeture à glissière sur les vêtements: la fermeture à glissière des vêtements a 2 côtés et chaque côté a un tas de dents. Lorsque vous allez dans une direction, la fermeture à glissière énumère (parcourt) les deux côtés et ferme la fermeture à glissière en serrant les dents. Quand tu vas dans l'autre sens, ça ouvre les dents. Vous terminez par une fermeture éclair ouverte ou fermée.

C'est la même idée avec la Zipméthode. Prenons un exemple où nous avons deux collections. L'un contient des lettres et l'autre le nom d'un aliment qui commence par cette lettre. Par souci de clarté, je les appelle leftSideOfZipperet rightSideOfZipper. Voici le code.

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

Notre tâche est de produire une collection dont la lettre du fruit est séparée par un :et son nom. Comme ça:

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

Zipà la rescousse. Pour suivre notre terminologie de la fermeture à glissière, nous appellerons ce résultat closedZipperet les éléments de la fermeture à glissière gauche que nous appellerons leftToothet le côté droit, nous appellerons righToothpour des raisons évidentes:

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

Dans ce qui précède, nous énumérons (voyageant) le côté gauche de la fermeture à glissière et le côté droit de la fermeture à glissière et effectuons une opération sur chaque dent. L'opération que nous effectuons consiste à concaténer la dent gauche (lettre de l'aliment) avec a :, puis la dent de droite (nom de l'aliment). Nous faisons cela en utilisant ce code:

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

Le résultat final est le suivant:

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

Qu'est-il arrivé à la dernière lettre E?

Si vous énumérez (tirez) une vraie fermeture à glissière de vêtements et qu'un côté, peu importe le côté gauche ou le côté droit, a moins de dents que l'autre côté, que se passera-t-il? Eh bien, la fermeture éclair s'arrêtera là. La Zipméthode fera exactement la même chose: elle s'arrêtera une fois qu'elle aura atteint le dernier élément de chaque côté. Dans notre cas, le côté droit a moins de dents (noms des aliments) donc il s'arrêtera à "Donut".

CodageYoshi
la source
1
+1. Oui, le nom «Zip» peut prêter à confusion au début. Peut-être que "Interleave" ou "Weave" aurait été des noms plus descriptifs pour la méthode.
BACON
1
@bacon oui, mais je n'aurais pas pu utiliser mon exemple de fermeture à glissière;) Je pense qu'une fois que vous avez compris que c'est comme une fermeture à glissière, c'est assez simple après.
CodingYoshi
Bien que je sache exactement ce que fait la méthode d'extension Zip, j'ai toujours été curieux de savoir pourquoi elle a été nommée ainsi. Dans le jargon général du logiciel, zip a toujours signifié autre chose. Grande analogie :-) Vous devez avoir lu dans l'esprit du créateur.
Raghu Reddy Muttana
7

Je n'ai pas les points de représentant à publier dans la section commentaires, mais pour répondre à la question connexe:

Que faire si je veux que zip continue là où une liste manque d'éléments? Dans ce cas, l'élément de liste le plus court doit prendre la valeur par défaut. Dans ce cas, la sortie doit être A1, B2, C3, D0, E0. - liang 19 novembre 15 à 3:29

Ce que vous feriez est d'utiliser Array.Resize () pour compléter la séquence plus courte avec les valeurs par défaut, puis les Zip () ensemble.

Exemple de code:

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

Production:

A1
B2
C3
D0
E0

Veuillez noter que l'utilisation de Array.Resize () a une mise en garde : Redim Preserve en C #?

Si on ne sait pas quelle séquence sera la plus courte, une fonction peut être créée qui la résume:

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

Sortie de .Zip () avec ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Pour en revenir à la réponse principale de la question d'origine , une autre chose intéressante que l'on pourrait souhaiter faire (lorsque les longueurs des séquences à «zipper» sont différentes) est de les joindre de telle manière que la fin de la liste correspond au lieu du haut. Ceci peut être accompli en "sautant" le nombre approprié d'éléments en utilisant .Skip ().

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

Production:

C1
D2
E3
un invité plus étrange
la source
Le redimensionnement est un gaspillage, surtout si l'une des collections est volumineuse. Ce que vous voulez vraiment faire, c'est avoir une énumération qui continue après la fin de la collection, en la remplissant avec des valeurs vides à la demande (sans collection de sauvegarde). Vous pouvez le faire avec: 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
Il semble que je ne suis pas sûr de savoir comment mettre en forme avec succès le code dans un commentaire ...
Pagefault
7

Beaucoup de réponses ici démontrent Zip, mais sans vraiment expliquer un cas d'utilisation réel qui motiverait l'utilisation de Zip.

Un modèle particulièrement courant qui Zipest fantastique pour itérer sur des paires successives de choses. Cela se fait par un itérer dénombrable Xavec lui - même, en sautant 1 élément: x.Zip(x.Skip(1). Exemple visuel:

 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)

Ces paires successives sont utiles pour trouver les premières différences entre les valeurs. Par exemple, des paires successives de IEnumable<MouseXPosition>peuvent être utilisées pour produire IEnumerable<MouseXDelta>. De même, les boolvaleurs échantillonnées de a buttonpeuvent être interprétées en événements comme NotPressed/ Clicked/ Held/ Released. Ces événements peuvent ensuite générer des appels pour déléguer des méthodes. Voici un exemple:

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

Impressions:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked
Alexander - Réintégrer Monica
la source
6

Comme d'autres l'ont indiqué, Zip vous permet de combiner deux collections pour une utilisation dans d'autres instructions Linq ou une boucle foreach.

Les opérations qui nécessitaient auparavant une boucle for et deux tableaux peuvent désormais être effectuées dans une boucle foreach à l'aide d'un objet anonyme.

Un exemple que je viens de découvrir, c'est un peu idiot, mais qui pourrait être utile si la parallélisation était bénéfique serait une traversée de file d'attente sur une seule ligne avec des effets secondaires:

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 représente les éléments actuels ou retirés de la file d'attente dans une file d'attente (le dernier élément est tronqué par Zip). timeSegments.Skip (1) représente les éléments suivants ou aperçus dans une file d'attente. La méthode Zip combine ces deux éléments en un seul objet anonyme avec une propriété Next et Current. Ensuite, nous filtrons avec Where et apportons des modifications avec AsParallel (). ForAll. Bien sûr, le dernier bit pourrait simplement être une instruction foreach régulière ou une autre instruction Select qui renvoie les segments de temps incriminés.

Novaterata
la source
3

La méthode Zip vous permet de «fusionner» deux séquences non liées, en utilisant un fournisseur de fonctions de fusion par vous, l'appelant. L'exemple sur MSDN est en fait assez bon pour démontrer ce que vous pouvez faire avec Zip. Dans cet exemple, vous prenez deux séquences arbitraires et non liées et vous les combinez à l'aide d'une fonction arbitraire (dans ce cas, il suffit de concaténer les éléments des deux séquences en une seule chaîne).

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
la source
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
la source