71 votes

Comment parcourir deux IEnumerables simultanément?

Je dois énumérer les éléments IEnumerable<A> list1 et IEnumerable<B> list2 et souhaiter les parcourir simultanément, comme ceci:

 foreach((a, b) in (list1, list2)) {
  // use a and b
}
 

S'ils ne contiennent pas le même nombre d'éléments, une exception doit être levée.

Quelle est la meilleure façon de procéder?

58voto

Jon Skeet Points 692016

Vous voulez quelque chose comme l'opérateur Zip LINQ - mais la version de .NET 4 ne fait que tronquer à la fin de l'une des séquences.

L' implémentation MoreLINQ a une méthode EquiZip qui renvoie à la place un InvalidOperationException .

 var zipped = list1.EquiZip(list2, (a, b) => new { a, b });

foreach (var element in zipped)
{
    // use element.a and element.b
}
 

37voto

Lasse V. Karlsen Points 148037

Voici une implémentation de cette opération, généralement appelée Zip:

 using System;
using System.Collections.Generic;

namespace SO2721939
{
    public sealed class ZipEntry<T1, T2>
    {
        public ZipEntry(int index, T1 value1, T2 value2)
        {
            Index = index;
            Value1 = value1;
            Value2 = value2;
        }

        public int Index { get; private set; }
        public T1 Value1 { get; private set; }
        public T2 Value2 { get; private set; }
    }

    public static class EnumerableExtensions
    {
        public static IEnumerable<ZipEntry<T1, T2>> Zip<T1, T2>(
            this IEnumerable<T1> collection1, IEnumerable<T2> collection2)
        {
            if (collection1 == null)
                throw new ArgumentNullException("collection1");
            if (collection2 == null)
                throw new ArgumentNullException("collection2");

            int index = 0;
            using (IEnumerator<T1> enumerator1 = collection1.GetEnumerator())
            using (IEnumerator<T2> enumerator2 = collection2.GetEnumerator())
            {
                while (enumerator1.MoveNext() && enumerator2.MoveNext())
                {
                    yield return new ZipEntry<T1, T2>(
                        index, enumerator1.Current, enumerator2.Current);
                    index++;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int[] numbers = new[] { 1, 2, 3, 4, 5 };
            string[] names = new[] { "Bob", "Alice", "Mark", "John", "Mary" };

            foreach (var entry in numbers.Zip(names))
            {
                Console.Out.WriteLine(entry.Index + ": "
                    + entry.Value1 + "-" + entry.Value2);
            }
        }
    }
}
 

Pour que cela jette une exception si une seule des séquences manque de valeurs, changez la boucle while de la manière suivante:

 while (true)
{
    bool hasNext1 = enumerator1.MoveNext();
    bool hasNext2 = enumerator2.MoveNext();
    if (hasNext1 != hasNext2)
        throw new InvalidOperationException("One of the collections ran " +
            "out of values before the other");
    if (!hasNext1)
        break;

    yield return new ZipEntry<T1, T2>(
        index, enumerator1.Current, enumerator2.Current);
    index++;
}
 

22voto

Matt Greer Points 29401

En bref, le langage n’offre aucun moyen propre de le faire. L'énumération a été conçue pour être effectuée sur un énumérable à la fois. Vous pouvez facilement imiter ce que foreach fait pour vous:

 IEnumerator<A> list1enum = list1.GetEnumerator();
IEnumerator<B> list2enum = list2.GetEnumerator();    
while(list1enum.MoveNext() && list2enum.MoveNext()) {
        // list1enum.Current and list2enum.Current point to each current item
    }
 

Que faire s'ils sont de longueur différente, c'est à vous de décider. Peut-être trouver quels sont les éléments qui restent après la boucle while et continuer à travailler avec celui-ci, lever une exception s'ils doivent avoir la même longueur, etc.

7voto

Anthony Pegram Points 58528

Dans .NET 4, vous pouvez utiliser la méthode d’extension .Zip sur IEnumerable<T>

 IEnumerable<int> list1 = Enumerable.Range(0, 100);
IEnumerable<int> list2 = Enumerable.Range(100, 100);

foreach (var item in list1.Zip(list2, (a, b) => new { a, b }))
{
    // use item.a and item.b
}
 

Il ne jettera cependant pas sur des longueurs inégales. Vous pouvez toujours tester cela, cependant.

3voto

jpabluz Points 942

Aller avec IEnumerable.GetEnumerator, de sorte que vous pouvez déplacer autour de la énumérable. Notez que ce processus peut être vraiment mauvais comportement, et vous devez être prudent. Si vous souhaitez obtenir ce travail, aller avec cela, si vous voulez avoir de code faciles à gérer, utiliser deux foreach.

Vous pouvez créer un habillage de la classe ou de l'utilisation d'une bibliothèque (comme Jon Skeet l'indique) pour gérer cette fonctionnalité dans un plus générique façon, si vous allez l'utiliser plus d'une fois par le biais de votre code.

Le code pour ce que je suggère:

var firstEnum = aIEnumerable.GetEnumerator();
var secondEnum = bIEnumerable.GetEnumerator();

var firstEnumMoreItems = firstEnum.MoveNext();
var secondEnumMoreItems = secondEnum.MoveNext();    

while (firstEnumMoreItems && secondEnumMoreItems)
{
      // Do whatever.  
      firstEnumMoreItems = firstEnum.MoveNext();
      secondEnumMoreItems = secondEnum.MoveNext();   
}

if (firstEnumMoreItems || secondEnumMoreItems)
{
     Throw new Exception("One Enum is bigger");
}

// IEnumerator does not have a Dispose method, but IEnumerator<T> has.
if (firstEnum is IDisposable) { ((IDisposable)firstEnum).Dispose(); }
if (secondEnum is IDisposable) { ((IDisposable)secondEnum).Dispose(); }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X