Was ist die Verwendung der Enumerable.Zip-Erweiterungsmethode in Linq?


Antworten:


191

Der Zip-Operator führt die entsprechenden Elemente zweier Sequenzen mithilfe einer angegebenen Auswahlfunktion zusammen.

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

Ausgabe

A1
B2
C3

41
Ich mag diese Antwort, weil sie zeigt, was passiert, wenn die Anzahl der Elemente nicht übereinstimmt, ähnlich wie in der MSDN-Dokumentation
DLeh

2
Was ist, wenn Zip fortgesetzt werden soll, wenn in einer Liste keine Elemente mehr vorhanden sind? In diesem Fall sollte das kürzere Listenelement den Standardwert annehmen. Die Ausgabe erfolgt in diesem Fall A1, B2, C3, D0, E0.
Liang

2
@liang Zwei Möglichkeiten: A) Schreiben Sie Ihre eigene ZipAlternative. B) Schreiben auf ein Verfahren yield returnjedes Element der kürzeren Liste, und dann weiter yield returning auf defaultunbestimmte Zeit danach. (Option B erfordert, dass Sie im Voraus wissen, welche Liste kürzer ist.)
jpaugh

105

Zipdient zum Kombinieren von zwei Sequenzen zu einer. Zum Beispiel, wenn Sie die Sequenzen haben

1, 2, 3

und

10, 20, 30

und Sie möchten die Sequenz erhalten, die das Ergebnis der Multiplikation von Elementen an derselben Position in jeder Sequenz ist

10, 40, 90

du könntest sagen

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

Es wird als "Reißverschluss" bezeichnet, da Sie sich eine Sequenz als die linke Seite eines Reißverschlusses und die andere Sequenz als die rechte Seite des Reißverschlusses vorstellen Elemente der Sequenz) angemessen.


8
Auf jeden Fall die beste Erklärung hier.
Maxim Gershkovich

2
Liebte das Beispiel des Reißverschlusses. Es war so natürlich. Mein erster Eindruck war, ob es etwas mit Geschwindigkeit oder so etwas zu tun hat, als ob Sie mit Ihrem Auto durch eine Straße fahren.
RBT

23

Es durchläuft zwei Sequenzen und kombiniert ihre Elemente nacheinander zu einer einzigen neuen Sequenz. Sie nehmen also ein Element der Sequenz A, transformieren es mit dem entsprechenden Element aus Sequenz B und das Ergebnis bildet ein Element der Sequenz C.

Eine Möglichkeit, darüber nachzudenken, besteht darin, dass es ähnlich ist Select, außer dass Elemente aus einer einzelnen Sammlung transformiert werden und zwei Sammlungen gleichzeitig bearbeitet werden.

Aus dem MSDN-Artikel zur Methode :

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

Wenn Sie dies in imperativem Code tun würden, würden Sie wahrscheinlich Folgendes tun:

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

Oder wenn LINQ nicht Zipdrin wäre, könnten Sie dies tun:

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

Dies ist nützlich, wenn Sie Daten in einfachen, Array-ähnlichen Listen mit jeweils gleicher Länge und Reihenfolge verteilen und jeweils eine andere Eigenschaft derselben Objektgruppe beschreiben. Ziphilft Ihnen, diese Daten zu einer kohärenteren Struktur zusammenzufügen.

Wenn Sie also ein Array von Statusnamen und ein anderes Array ihrer Abkürzungen haben, können Sie diese Statewie folgt zu einer Klasse zusammenfassen:

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

Ich mochte diese Antwort auch, weil sie die Ähnlichkeit mitSelect
iliketocode

17

Lassen ZipSie sich NICHT vom Namen abschrecken. Es hat nichts mit dem Komprimieren zu tun, wie beim Komprimieren einer Datei oder eines Ordners (Komprimieren). Es hat seinen Namen von der Funktionsweise eines Reißverschlusses an der Kleidung: Der Reißverschluss an der Kleidung hat zwei Seiten und jede Seite hat ein paar Zähne. Wenn Sie in eine Richtung gehen, zählt der Reißverschluss beide Seiten auf (fährt) und schließt den Reißverschluss durch Zusammenbeißen der Zähne. Wenn Sie in die andere Richtung gehen, werden die Zähne geöffnet. Sie enden entweder mit einem offenen oder einem geschlossenen Reißverschluss.

Es ist die gleiche Idee mit der ZipMethode. Stellen Sie sich ein Beispiel vor, in dem wir zwei Sammlungen haben. Einer enthält Buchstaben und der andere den Namen eines Lebensmittels, das mit diesem Buchstaben beginnt. Aus Gründen der Klarheit rufe ich sie an leftSideOfZipperund rightSideOfZipper. Hier ist der Code.

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

Unsere Aufgabe ist es, eine Sammlung zu erstellen, bei der der Buchstabe der Frucht durch a :und ihren Namen getrennt ist. So was:

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

Zipzur Rettung. Um mit unserer Reißverschluss-Terminologie Schritt zu halten, werden wir dieses Ergebnis closedZipperund die Elemente des linken Reißverschlusses leftToothund die rechte Seite righToothaus offensichtlichen Gründen aufrufen :

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

Oben zählen wir die linke Seite des Reißverschlusses und die rechte Seite des Reißverschlusses auf (bewegen) und führen eine Operation an jedem Zahn durch. Die Operation, die wir durchführen, besteht darin, den linken Zahn (Lebensmittelbuchstabe) mit einem :und dann den rechten Zahn (Lebensmittelname) zu verketten . Wir machen das mit diesem Code:

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

Das Endergebnis ist folgendes:

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

Was ist mit dem letzten Buchstaben E passiert?

Wenn Sie einen echten Kleidungsreißverschluss aufzählen (ziehen) und eine Seite, egal ob die linke oder die rechte Seite, weniger Zähne als die andere Seite hat, was passiert dann? Nun, der Reißverschluss wird dort anhalten. Die ZipMethode macht genau das Gleiche: Sie stoppt, sobald sie das letzte Element auf beiden Seiten erreicht hat. In unserem Fall hat die rechte Seite weniger Zähne (Nahrungsnamen), so dass sie bei "Donut" endet.


1
+1. Ja, der Name "Zip" kann zunächst verwirrend sein. Vielleicht wären "Interleave" oder "Weave" aussagekräftigere Namen für die Methode gewesen.
Speck

1
@bacon ja, aber dann hätte ich mein Reißverschluss-Beispiel nicht verwenden können;) Ich denke, wenn Sie einmal herausgefunden haben, dass es wie ein Reißverschluss ist, ist es danach ziemlich einfach.
CodingYoshi

Obwohl ich genau wusste, was die Zip-Erweiterungsmethode bewirkt, war ich immer neugierig, warum sie so benannt wurde. Im allgemeinen Jargon der Software hat zip immer etwas anderes bedeutet. Tolle Analogie :-) Du musst die Gedanken des Schöpfers gelesen haben.
Raghu Reddy Muttana

7

Ich habe nicht die Wiederholungspunkte, die ich im Kommentarbereich posten kann, sondern um die entsprechende Frage zu beantworten:

Was ist, wenn Zip fortgesetzt werden soll, wenn in einer Liste keine Elemente mehr vorhanden sind? In diesem Fall sollte das kürzere Listenelement den Standardwert annehmen. Die Ausgabe erfolgt in diesem Fall A1, B2, C3, D0, E0. - Liang 19. November 15 um 3:29

Sie würden Array.Resize () verwenden, um die kürzere Sequenz mit Standardwerten aufzufüllen und sie dann zusammen zu zip ().

Codebeispiel:

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

Ausgabe:

A1
B2
C3
D0
E0

Bitte beachten Sie, dass die Verwendung von Array.Resize () eine Einschränkung hat : Redim Preserve in C #?

Wenn nicht bekannt ist, welche Sequenz die kürzere sein wird, kann eine Funktion erstellt werden, die sie zusammenfasst:

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

Ausgabe von Plain .Zip () neben ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Zurück zur Hauptantwort der ursprünglichen Frage : Eine weitere interessante Sache, die man möglicherweise tun möchte (wenn die Längen der zu "komprimierenden" Sequenzen unterschiedlich sind), besteht darin, sie so zu verbinden, dass das Ende der Liste erreicht wird Streichhölzer statt oben. Dies kann erreicht werden, indem die entsprechende Anzahl von Elementen mit .Skip () "übersprungen" wird.

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

Ausgabe:

C1
D2
E3

Das Ändern der Größe ist verschwenderisch, insbesondere wenn eine der Sammlungen groß ist. Was Sie wirklich tun möchten, ist eine Aufzählung, die nach dem Ende der Sammlung fortgesetzt wird und bei Bedarf mit leeren Werten gefüllt wird (ohne Hintergrundsammlung). Sie können das tun mit: 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

Ich bin mir nicht sicher, wie ich Code in einem Kommentar erfolgreich formatieren soll ...
Pagefault

7

Viele der Antworten hier zeigen Zip, aber ohne wirklich einen realen Anwendungsfall zu erklären, der die Verwendung von motivieren würde Zip.

Ein besonders häufiges Muster, das Zipsich hervorragend zum Durchlaufen aufeinanderfolgender Paar von Dingen eignet. Dies erfolgt durch Iteration einer Aufzählung Xmit sich selbst, wobei 1 Element übersprungen wird : x.Zip(x.Skip(1). Visuelles Beispiel:

 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)

Diese aufeinanderfolgenden Paare sind nützlich, um die ersten Unterschiede zwischen Werten zu finden. Zum Beispiel können aufeinanderfolgende Paare von IEnumable<MouseXPosition>verwendet werden, um zu produzieren IEnumerable<MouseXDelta>. In ähnlicher Weise können abgetastete boolWerte von a buttonin Ereignisse wie NotPressed/ Clicked/ Held/ interpretiert werden Released. Diese Ereignisse können dann Aufrufe zum Delegieren von Methoden auslösen. Hier ist ein Beispiel:

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

Drucke:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked

6

Wie bereits erwähnt, können Sie mit Zip zwei Sammlungen zur Verwendung in weiteren Linq-Anweisungen oder einer foreach-Schleife kombinieren.

Vorgänge, für die früher eine for-Schleife und zwei Arrays erforderlich waren, können jetzt in einer foreach-Schleife mit einem anonymen Objekt ausgeführt werden.

Ein Beispiel, das ich gerade entdeckt habe, das irgendwie albern ist, aber nützlich sein könnte, wenn die Parallelisierung vorteilhaft wäre, wäre eine Warteschlangenüberquerung mit einer Zeile mit Nebenwirkungen:

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äsentiert die aktuellen oder in die Warteschlange gestellten Elemente in einer Warteschlange (das letzte Element wird von Zip abgeschnitten). timeSegments.Skip (1) repräsentiert die nächsten oder Peek-Elemente in einer Warteschlange. Die Zip-Methode kombiniert diese beiden zu einem einzigen anonymen Objekt mit den Eigenschaften Next und Current. Dann filtern wir mit Where und nehmen Änderungen mit AsParallel () vor. ForAll. Natürlich kann das letzte Bit nur eine reguläre foreach- oder eine andere Select-Anweisung sein, die die fehlerhaften Zeitsegmente zurückgibt.


3

Mit der Zip-Methode können Sie zwei nicht verwandte Sequenzen mithilfe eines Anbieters für Zusammenführungsfunktionen von Ihnen, dem Anrufer, "zusammenführen". Das Beispiel auf MSDN zeigt ziemlich gut, was Sie mit Zip tun können. In diesem Beispiel nehmen Sie zwei beliebige, nicht verwandte Sequenzen und kombinieren sie mit einer beliebigen Funktion (in diesem Fall verketten Sie nur Elemente aus beiden Sequenzen zu einer einzigen Zeichenfolge).

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.