Itérer deux listes ou tableaux avec une instruction ForEach en C #


142

Ceci juste pour les connaissances générales:

Si j'ai deux, disons, List , et que je veux itérer les deux avec la même boucle foreach, pouvons-nous faire cela?

Éditer

Juste pour clarifier, je voulais faire ceci:

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

Mais avec un foreach =)


10
Le mot important ici est "zip".
Mark Byers

3
Voulez-vous parcourir deux listes en parallèle ? Ou voulez-vous parcourir une première liste, puis l'autre (avec une seule instruction)?
Pavel Minaev

Je pense que votre chemin est meilleur que le zip
Alexander

Réponses:


275

Ceci est connu comme une opération Zip et sera pris en charge dans .NET 4.

Avec cela, vous seriez capable d'écrire quelque chose comme:

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

Comme alternative au type anonyme avec les champs nommés, vous pouvez également enregistrer sur des accolades en utilisant un Tuple et son assistant Tuple.Create statique:

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


2
Je ne savais rien de ces opérations Zip, je vais faire une petite recherche sur ce sujet. Merci!
Hugo

4
@Hugo: C'est une construction standard en programmation fonctionnelle :)
Mark Seemann

Vous devrez également utiliser System.Linq;
Jahmic

5
Depuis C # 7, vous pouvez également utiliser un ValueTuple (voir stackoverflow.com/a/45617748 ) au lieu de types anonymes ou Tuple.Create. Ie foreach ((var number, var word) in numbers.Zip(words, (n, w) => (n, w))) { ... }.
Erlend Graff

14

Si vous ne souhaitez pas attendre .NET 4.0, vous pouvez implémenter votre propre Zipméthode. Ce qui suit fonctionne avec .NET 2.0. Vous pouvez ajuster l'implémentation en fonction de la manière dont vous souhaitez gérer le cas où les deux énumérations (ou listes) ont des longueurs différentes; celui-ci continue jusqu'à la fin de l'énumération plus longue, retournant les valeurs par défaut pour les éléments manquants de l'énumération plus courte.

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
Belle méthode! :). Vous pouvez effectuer quelques ajustements pour utiliser la même signature que la méthode .NET 4 Zip msdn.microsoft.com/en-us/library/dd267698.aspx et renvoyer resultSelector (premier, deuxième) au lieu d'un KVP.
Martín Coll

Notez que cette méthode ne supprime pas ses énumérateurs qui pourraient devenir un problème, par exemple si elle est utilisée avec des énumérables sur les lignes de fichiers ouverts.
Lii

11

Vous pouvez utiliser Union ou Concat, le premier supprime les doublons, le second ne le fait pas

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

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

Un autre problème lié à l'utilisation d'une Union est qu'elle peut rejeter des instances si elles sont évaluées comme étant égales. Ce n'est peut-être pas toujours ce que vous voulez.
Mark Seemann

1
Je pense que son intention était d'utiliser des collections du même type,
albertein

@Mark Seemann, je l'ai déjà souligné, il pourrait également utiliser Concat
albertein

Comme Union, Concat ne fonctionne que si les deux listes sont du même type. Je ne peux pas dire si c'est ce dont l'OP a besoin ou non, cependant ...
Mark Seemann

Cela crée une nouvelle liste qui contient tous les éléments. C'est une perte de mémoire. Utilisez plutôt le Linq Concat.
Drew Noakes

3

Voici une méthode d'extension IEnumerable <> personnalisée qui peut être utilisée pour parcourir simultanément deux listes.

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

Depuis C # 7, vous pouvez utiliser des tuples ...

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
Que se passe-t-il lorsque les deux listes ne sont pas de la même longueur dans cette situation?
John août

Avec (x, y) => (x, y)on peut utiliser named tuple.xet tuple.yqui est élégant. Donc, la deuxième forme pourrait aussi être(Num, Word) => (Num, Word)
dashesy

2
@JohnAugust Il se termine après avoir parcouru la séquence la plus courte. De la documentation: "Si les séquences n'ont pas le même nombre d'éléments, la méthode fusionne les séquences jusqu'à ce qu'elle atteigne la fin de l'un d'entre eux. Par exemple, si une séquence comporte trois éléments et l'autre quatre, la séquence de résultat sera n'ont que trois éléments. "
gregsmi

0

Non, vous devrez utiliser une boucle for pour cela.

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

Tu ne peux pas faire quelque chose comme

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

Et s'ils ont des comptes différents?
Drew Noakes

Ensuite, un foreach qui accepterait une liste arbitraire d'énumérables ne fonctionnerait pas aussi bien, rendant ainsi le tout inutile.
Maximilian Mayerl

0

Si vous voulez un élément avec celui correspondant, vous pouvez le faire

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

Cela retournera vrai si chaque élément est égal à celui correspondant sur la deuxième liste

Si c'est presque mais pas tout à fait ce que vous voulez, cela vous aiderait si vous développiez plus.


0

Cette méthode fonctionnerait pour une implémentation de liste et pourrait être implémentée comme une méthode d'extension.

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

Vous pouvez également simplement utiliser une variable locale entière si les listes ont la même longueur:

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

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

-1

Vous pouvez également effectuer les opérations suivantes:

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

Remarque: la longueur de listAdoit être la même que celle de listB.


-3

Je comprends / j'espère que les listes ont la même longueur: Non, votre seul pari va avec un vieux standard pour la boucle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.