Jakie jest zastosowanie metody rozszerzenia Enumerable.Zip w Linq?


Odpowiedzi:


199

Operator Zip scala odpowiednie elementy dwóch sekwencji przy użyciu określonej funkcji selektora.

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

47
I jak tej odpowiedzi, ponieważ pokazuje, co się dzieje, gdy liczba elementów nie zgadza się, podobnie jak w dokumentacji MSDN
DLeh

2
co jeśli chcę, aby zip był kontynuowany, gdy na jednej liście zabraknie elementów? w takim przypadku krótszy element listy powinien przyjąć wartość domyślną. Wyjście w tym przypadku to A1, B2, C3, D0, E0.
liang

2
@liang Dwie możliwości: A) Napisz własną Zipalternatywę. B) napisać metodę do yield returnkażdego elementu krótszej listy, a następnie kontynuować yield returning defaultnieskończoność później. (Opcja B wymaga wcześniejszego poinformowania, która lista jest krótsza.)
jpaugh

105

Zipsłuży do łączenia dwóch sekwencji w jedną. Na przykład, jeśli masz sekwencje

1, 2, 3

i

10, 20, 30

i chcesz uzyskać sekwencję będącą wynikiem mnożenia elementów w tej samej pozycji w każdej sekwencji

10, 40, 90

można powiedzieć

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

Nazywa się to „zamkiem błyskawicznym”, ponieważ myślisz o jednej sekwencji jako o lewej stronie zamka, a o drugiej jako o prawej stronie zamka błyskawicznego, a operator zamka błyskawicznego pociągnie obie strony razem, parując zęby ( elementy sekwencji) odpowiednio.


8
Zdecydowanie najlepsze wyjaśnienie tutaj.
Maxim Gershkovich

2
Bardzo podobał mi się przykład zamka błyskawicznego. To było takie naturalne. Moje pierwsze wrażenie było takie, że ma to coś wspólnego z prędkością lub czymś w tym rodzaju, tak jakbyś przemknął przez ulicę w samochodzie.
RBT

23

Iteruje przez dwie sekwencje i łączy ich elementy, jeden po drugim, w jedną nową sekwencję. Więc bierzesz element ciągu A, przekształcasz go odpowiednim elementem z sekwencji B, a wynik tworzy element ciągu C.

Można o tym pomyśleć, że jest podobny do tego Select, z wyjątkiem tego, że zamiast przekształcać elementy z jednej kolekcji, działa na dwóch kolekcjach jednocześnie.

Z artykułu MSDN na temat metody :

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

Gdybyś miał to zrobić w kodzie imperatywnym, prawdopodobnie zrobiłbyś coś takiego:

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

Lub jeśli LINQ go nie ma Zip, możesz to zrobić:

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

Jest to przydatne, gdy dane są rozmieszczone na prostych listach przypominających tablice, z których każda ma tę samą długość i kolejność, a każda z nich opisuje inną właściwość tego samego zestawu obiektów. Zippomaga połączyć te fragmenty danych w bardziej spójną strukturę.

Więc jeśli masz tablicę nazw stanów i inną tablicę ich skrótów, możesz zebrać je w Stateklasę, taką jak ta:

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

Podobała mi się również ta odpowiedź, ponieważ wspomina o podobieństwie doSelect
iliketocode.

17

NIE pozwól, aby nazwa ZipCię zniechęciła. Nie ma to nic wspólnego z kompresowaniem, jak z kompresowaniem pliku lub folderu (kompresja). Swoją nazwę zawdzięcza działaniu zamka błyskawicznego na ubraniu: zamek błyskawiczny na ubraniach ma 2 boki, a po każdej stronie kilka zębów. Kiedy idziesz w jednym kierunku, zamek błyskawiczny wylicza (przesuwa) obie strony i zamyka zamek, zaciskając zęby. Kiedy idziesz w innym kierunku, otwiera zęby. Kończysz otwartym lub zamkniętym zamkiem błyskawicznym.

Tak samo jest z Zipmetodą. Rozważmy przykład, w którym mamy dwie kolekcje. Jedna zawiera litery, a druga nazwę produktu spożywczego, która zaczyna się od tej litery. Dla jasności nazywam je leftSideOfZipperi rightSideOfZipper. Oto kod.

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

Naszym zadaniem jest wyprodukowanie jednej kolekcji, w której litera owocu oddzielona jest znakiem :a oraz jego nazwą. Lubię to:

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

Zipna pomoc. Aby nadążyć za naszą terminologią dotyczącą zamków błyskawicznych, nazwiemy ten wynik closedZipperi elementy lewego zamka będziemy nazywać, leftTootha prawą stronę nazwiemy righToothz oczywistych powodów:

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

W powyższym wyliczamy (przesuwamy) lewą stronę zamka i prawą stronę zamka oraz wykonujemy operację na każdym zębie. Operacja, którą wykonujemy, to połączenie lewego zęba (litera pokarmu) z a, :a następnie prawego zęba (nazwa pokarmu). Robimy to za pomocą tego kodu:

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

Efekt końcowy jest taki:

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

Co się stało z ostatnią literą E?

Jeśli wyliczasz (ciągniesz) prawdziwy zamek do ubrania i jedną stronę, nieważne, czy lewa strona, czy prawa strona, ma mniej zębów niż druga strona, co się stanie? Zamek błyskawiczny się tam zatrzyma. ZipMetoda zrobi dokładnie to samo: Będzie zatrzymać po osiągnięciu ostatniego elementu po obu stronach. W naszym przypadku prawa strona ma mniej zębów (nazw potraw), więc zatrzyma się na „Donut”.


1
+1. Tak, na początku nazwa „Zip” może być myląca. Być może „Interleave” lub „Weave” byłyby bardziej opisowymi nazwami metody.
BACON

1
@bacon tak, ale wtedy nie byłbym w stanie użyć mojego przykładu z zamkiem błyskawicznym;) Myślę, że kiedy już zrozumiesz, że jest jak zamek błyskawiczny, potem jest całkiem prosty.
CodingYoshi

Chociaż dokładnie wiedziałem, co robi metoda rozszerzenia Zip, zawsze byłem ciekawy, dlaczego została tak nazwana. W ogólnym żargonie oprogramowania zip zawsze oznaczało coś innego. Świetna analogia :-) Musisz przeczytać w myślach twórcy.
Raghu Reddy Muttana,

9

Wiele odpowiedzi tutaj pokazuje Zip, ale tak naprawdę bez wyjaśnienia rzeczywistego przypadku użycia, który motywowałby użycie Zip.

Jeden szczególnie powszechny wzorzec, który Zipjest fantastyczny do iterowania po kolejnych parach rzeczy. Odbywa się to przez iteracji jest przeliczalny Xz siebie, omijając 1 elementu: x.Zip(x.Skip(1). Przykład wizualny:

 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)

Te kolejne pary są przydatne do znajdowania pierwszych różnic między wartościami. Na przykład IEnumable<MouseXPosition>do produkcji można użyć kolejnych par IEnumerable<MouseXDelta>. Podobnie próbkowane boolwartości a buttonmogą być interpretowane jako zdarzenia takie jak NotPressed/ Clicked/ Held/ Released. Te zdarzenia mogą następnie kierować wywołania do metod delegowania. Oto przykład:

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

Wydruki:

NotPressesd
NotPressesd
NotPressesd
Clicked
Held
Held
Released
Clicked
Released
NotPressesd
Clicked

7

Nie mam punktów przedstawiciela do opublikowania w sekcji komentarzy, ale aby odpowiedzieć na powiązane pytanie:

Co jeśli chcę, aby zip był kontynuowany, gdy na jednej liście zabrakło elementów? W takim przypadku krótszy element listy powinien przyjąć wartość domyślną. Wyjście w tym przypadku to A1, B2, C3, D0, E0. - liang listopad 19 '15 o 3:29

To, co byś zrobił, to użycie Array.Resize () do wypełnienia krótszej sekwencji wartościami domyślnymi, a następnie Zip () je razem.

Przykład kodu:

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

Wynik:

A1
B2
C3
D0
E0

Należy pamiętać, że użycie Array.Resize () ma zastrzeżenie : Redim Preserve in C #?

Jeśli nie wiadomo, która sekwencja będzie krótsza, można utworzyć funkcję, która ją ogranicza:

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

Wyjście zwykłego .Zip () wraz z ZipDefault ():

A1 A1
B2 B2
C3 C3
   D0
   E0

Wracając do głównej odpowiedzi na pierwotne pytanie , kolejną interesującą rzeczą, którą można by chcieć zrobić (gdy długości sekwencji do „spakowania” są różne) jest połączenie ich w taki sposób, aby koniec listy mecze zamiast góry. Można to osiągnąć "pomijając" odpowiednią liczbę elementów za pomocą .Skip ().

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

Wynik:

C1
D2
E3

Zmiana rozmiaru jest marnotrawstwem, zwłaszcza jeśli któraś z kolekcji jest duża. To, co naprawdę chcesz zrobić, to mieć wyliczenie, które jest kontynuowane po zakończeniu kolekcji, wypełniając ją pustymi wartościami na żądanie (bez kolekcji zapasowej). Możesz to zrobić za pomocą: 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

Wygląda na to, że nie jestem pewien, jak pomyślnie sformatować kod w komentarzu ...
Pagefault

6

Jak powiedzieli inni, Zip pozwala łączyć dwie kolekcje do wykorzystania w dalszych instrukcjach Linq lub w pętli foreach.

Operacje, które kiedyś wymagały pętli for i dwóch tablic, można teraz wykonać w pętli foreach przy użyciu anonimowego obiektu.

Przykład, który właśnie odkryłem, jest trochę głupi, ale mógłby być przydatny, gdyby równoległość była korzystna, to przechodzenie kolejki pojedynczej linii z efektami ubocznymi:

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 reprezentuje bieżące lub usunięte z kolejki elementy w kolejce (ostatni element jest obcięty przez Zip). timeSegments.Skip (1) reprezentuje następne lub podglądane elementy w kolejce. Metoda Zip łączy te dwa w jeden anonimowy obiekt z właściwościami Next i Current. Następnie filtrujemy za pomocą Where i wprowadzamy zmiany za pomocą AsParallel (). ForAll. Oczywiście ostatni bit może być zwykłą instrukcją foreach lub inną instrukcją Select, która zwraca niewłaściwe segmenty czasu.


3

Metoda Zip pozwala na „scalenie” dwóch niepowiązanych ze sobą sekwencji przy użyciu dostawcy funkcji scalającej przez Ciebie, osobę dzwoniącą. Przykład w witrynie MSDN jest całkiem dobry w zademonstrowaniu, co można zrobić za pomocą Zipa. W tym przykładzie bierzesz dwie dowolne, niepowiązane sekwencje i łączysz je za pomocą dowolnej funkcji (w tym przypadku po prostu łączysz elementy z obu sekwencji w jeden ciąg).

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.