Wykonaj iterację dwóch list lub tablic za pomocą jednej instrukcji ForEach w języku C #


142

To tylko dla ogólnej wiedzy:

Jeśli mam dwie, powiedzmy List , i chcę iterować obie z tą samą pętlą foreach, czy możemy to zrobić?

Edytować

Dla wyjaśnienia chciałem to zrobić:

List<String> listA = new List<string> { "string", "string" };
List<String> listB = new List<string> { "string", "string" };

for(int i = 0; i < listA.Count; i++)
    listB[i] = listA[i];

Ale z foreach =)


10
Ważnym tutaj słowem jest „zip”.
Mark Byers

3
Czy chcesz iterować dwie listy równolegle ? A może chcesz iterować najpierw jedną listę, a potem drugą (za pomocą jednej instrukcji)?
Pavel Minaev

Myślę, że twój sposób wygląda lepiej niż zip
Alexander

Odpowiedzi:


274

Jest to znane jako operacja Zip i będzie obsługiwane w .NET 4.

Dzięki temu mógłbyś napisać coś takiego:

var numbers = new [] { 1, 2, 3, 4 };
var words = new [] { "one", "two", "three", "four" };

var numbersAndWords = numbers.Zip(words, (n, w) => new { Number = n, Word = w });
foreach(var nw in numbersAndWords)
{
    Console.WriteLine(nw.Number + nw.Word);
}

Alternatywą dla typu anonimowego z nazwanymi polami jest również oszczędność na nawiasach klamrowych przy użyciu klasy pomocniczej Tuple i jej statycznej wartości Tuple.

foreach (var nw in numbers.Zip(words, Tuple.Create)) 
{
    Console.WriteLine(nw.Item1 + nw.Item2);
}


2
Nie wiedziałem nic o tych operacjach Zip, zrobię małe badanie na ten temat. Dzięki!
Hugo

4
@Hugo: To standardowa konstrukcja w programowaniu funkcjonalnym :)
Mark Seemann

Będziesz także musiał użyć System.Linq;
Jahmic

5
Od C # 7 można również użyć ValueTuple (zobacz stackoverflow.com/a/45617748 ) zamiast typów anonimowych lub Tuple.Create. Tj foreach ((var number, var word) in numbers.Zip(words, (n, w) => (n, w))) { ... }.
Erlend Graff,

14

Jeśli nie chcesz czekać na .NET 4.0, możesz zaimplementować własną Zipmetodę. Poniższe działa z .NET 2.0. Możesz dostosować implementację w zależności od tego, jak chcesz obsłużyć przypadek, w którym dwa wyliczenia (lub listy) mają różne długości; ten jest kontynuowany do końca dłuższego wyliczenia, zwracając domyślne wartości brakujących elementów z krótszego wyliczenia.

static IEnumerable<KeyValuePair<T, U>> Zip<T, U>(IEnumerable<T> first, IEnumerable<U> second)
{
    IEnumerator<T> firstEnumerator = first.GetEnumerator();
    IEnumerator<U> secondEnumerator = second.GetEnumerator();

    while (firstEnumerator.MoveNext())
    {
        if (secondEnumerator.MoveNext())
        {
            yield return new KeyValuePair<T, U>(firstEnumerator.Current, secondEnumerator.Current);
        }
        else
        {
            yield return new KeyValuePair<T, U>(firstEnumerator.Current, default(U));
        }
    }
    while (secondEnumerator.MoveNext())
    {
        yield return new KeyValuePair<T, U>(default(T), secondEnumerator.Current);
    }
}

static void Test()
{
    IList<string> names = new string[] { "one", "two", "three" };
    IList<int> ids = new int[] { 1, 2, 3, 4 };

    foreach (KeyValuePair<string, int> keyValuePair in ParallelEnumerate(names, ids))
    {
        Console.WriteLine(keyValuePair.Key ?? "<null>" + " - " + keyValuePair.Value.ToString());
    }
}

1
Niezła metoda! :). Możesz wprowadzić kilka poprawek, aby użyć tego samego podpisu, co metoda Zip platformy .NET 4 msdn.microsoft.com/en-us/library/dd267698.aspx i zwrócić resultSelector (pierwszy, drugi) zamiast KVP.
Martín Coll,

Zauważ, że ta metoda nie usuwa swoich modułów wyliczających, co mogłoby stać się problemem, np. Jeśli jest używana z wyliczeniami w wierszach otwartych plików.
Lii

11

Możesz użyć Union lub Concat, pierwszy usuwa duplikaty, drugi nie

foreach (var item in List1.Union(List1))
{
   //TODO: Real code goes here
}

foreach (var item in List1.Concat(List1))
{
   //TODO: Real code goes here
}

Innym problemem związanym z używaniem Union jest to, że może ona odrzucać wystąpienia, jeśli zostaną ocenione jako równe. To nie zawsze może być tym, czego chcesz.
Mark Seemann

1
Trudno mi powiedzieć, że jego zamiarem było wykorzystanie kolekcji tego samego typu,
albertein

@Mark Seemann, już wskazałem, że może również użyć Concat
albertein

Podobnie jak Union, Concat działa tylko wtedy, gdy obie listy są tego samego typu. Nie mogę powiedzieć, czy tego PO potrzebuje, czy nie ...
Mark Seemann

Spowoduje to utworzenie nowej listy zawierającej wszystkie elementy. To strata pamięci. Zamiast tego użyj Linq Concat.
Drew Noakes

3

Oto niestandardowa metoda rozszerzenia IEnumerable <>, której można użyć do jednoczesnego przechodzenia przez dwie listy.

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
    public static class LinqCombinedSort
    {
        public static void Test()
        {
            var a = new[] {'a', 'b', 'c', 'd', 'e', 'f'};
            var b = new[] {3, 2, 1, 6, 5, 4};

            var sorted = from ab in a.Combine(b)
                         orderby ab.Second
                         select ab.First;

            foreach(char c in sorted)
            {
                Console.WriteLine(c);
            }
        }

        public static IEnumerable<Pair<TFirst, TSecond>> Combine<TFirst, TSecond>(this IEnumerable<TFirst> s1, IEnumerable<TSecond> s2)
        {
            using (var e1 = s1.GetEnumerator())
            using (var e2 = s2.GetEnumerator())
            {
                while (e1.MoveNext() && e2.MoveNext())
                {
                    yield return new Pair<TFirst, TSecond>(e1.Current, e2.Current);
                }
            }

        }


    }
    public class Pair<TFirst, TSecond>
    {
        private readonly TFirst _first;
        private readonly TSecond _second;
        private int _hashCode;

        public Pair(TFirst first, TSecond second)
        {
            _first = first;
            _second = second;
        }

        public TFirst First
        {
            get
            {
                return _first;
            }
        }

        public TSecond Second
        {
            get
            {
                return _second;
            }
        }

        public override int GetHashCode()
        {
            if (_hashCode == 0)
            {
                _hashCode = (ReferenceEquals(_first, null) ? 213 : _first.GetHashCode())*37 +
                            (ReferenceEquals(_second, null) ? 213 : _second.GetHashCode());
            }
            return _hashCode;
        }

        public override bool Equals(object obj)
        {
            var other = obj as Pair<TFirst, TSecond>;
            if (other == null)
            {
                return false;
            }
            return Equals(_first, other._first) && Equals(_second, other._second);
        }
    }

}

3

Od C # 7 możesz używać krotek ...

int[] nums = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three", "four" };

foreach (var tuple in nums.Zip(words, (x, y) => (x, y)))
{
    Console.WriteLine($"{tuple.Item1}: {tuple.Item2}");
}

// or...
foreach (var tuple in nums.Zip(words, (x, y) => (Num: x, Word: y)))
{
    Console.WriteLine($"{tuple.Num}: {tuple.Word}");
}

1
Co się dzieje, gdy dwie listy nie są tej samej długości w tej sytuacji?
Jan sierpień

Dzięki temu (x, y) => (x, y)możemy użyć nazwanego tuple.xi tuple.yeleganckiego. Więc druga forma też mogłaby być(Num, Word) => (Num, Word)
dashesy

2
@JohnAugust Kończy się po przejściu krótszej sekwencji. From docs: „Jeśli sekwencje nie mają takiej samej liczby elementów, metoda łączy sekwencje, aż osiągnie koniec jednego z nich. Na przykład, jeśli jedna sekwencja ma trzy elementy, a druga cztery, sekwencja wynikowa będzie mają tylko trzy elementy. "
gregsmi

0

Nie, musiałbyś do tego użyć pętli for.

for (int i = 0; i < lst1.Count; i++)
{
    //lst1[i]...
    //lst2[i]...
}

Nie możesz zrobić czegoś takiego

foreach (var objCurrent1 int lst1, var objCurrent2 in lst2)
{
    //...
}

A co, jeśli mają różne liczby?
Drew Noakes

Wtedy foreach, który zaakceptowałby dowolną listę wyliczeń, nie działałby tak dobrze, czyniąc całość bezużyteczną.
Maximilian Mayerl

0

Jeśli chcesz mieć jeden element z odpowiednim, możesz to zrobić

Enumerable.Range(0, List1.Count).All(x => List1[x] == List2[x]);

To zwróci prawdę, jeśli każdy element jest równy odpowiadającemu elementowi na drugiej liście

Jeśli to prawie, ale nie do końca to, czego chcesz, pomogłoby, gdybyś opracował więcej.


0

Ta metoda działałaby w przypadku implementacji listy i mogłaby zostać wdrożona jako metoda rozszerzająca.

public void TestMethod()
{
    var first = new List<int> {1, 2, 3, 4, 5};
    var second = new List<string> {"One", "Two", "Three", "Four", "Five"};

    foreach(var value in this.Zip(first, second, (x, y) => new {Number = x, Text = y}))
    {
        Console.WriteLine("{0} - {1}",value.Number, value.Text);
    }
}

public IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(List<TFirst> first, List<TSecond> second, Func<TFirst, TSecond, TResult> selector)
{
    if (first.Count != second.Count)
        throw new Exception();  

    for(var i = 0; i < first.Count; i++)
    {
        yield return selector.Invoke(first[i], second[i]);
    }
}

0

Możesz również po prostu użyć lokalnej zmiennej całkowitej, jeśli listy mają tę samą długość:

List<classA> listA = fillListA();
List<classB> listB = fillListB();

var i = 0;
foreach(var itemA in listA)
{
    Console.WriteLine(itemA  + listB[i++]);
}

-1

Możesz też wykonać następujące czynności:

var i = 0;
foreach (var itemA in listA)
{
  Console.WriteLine(itemA + listB[i++]);
}

Uwaga: długość listAmusi być taka sama z listB.


-3

Rozumiem / mam nadzieję, że listy mają taką samą długość: Nie, Twój jedyny zakład to zwykły stary standard pętli.

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.