Как перебрать два IEnumerables одновременно? - PullRequest
53 голосов
/ 27 апреля 2010

У меня есть два перечислимых значения: IEnumerable<A> list1 и IEnumerable<B> list2.Я хотел бы выполнять итерации по ним одновременно, например:

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

Если они не содержат одинаковое количество элементов, следует выдать исключение.

Что является лучшим способом сделатьэто?

Ответы [ 8 ]

50 голосов
/ 27 апреля 2010

Вы хотите что-то вроде оператора Zip LINQ - но версия в .NET 4 всегда просто усекается, когда завершается любая последовательность.

Реализация MoreLINQ имеет метод EquiZip, который вместо этого выдает InvalidOperationException.

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

foreach (var element in zipped)
{
    // use element.a and element.b
}
32 голосов
/ 27 апреля 2010

Вот реализация этой операции, обычно называемая 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);
            }
        }
    }
}

Чтобы сделать исключение, если только в одной из последовательностей заканчиваются значения, измените цикл while так:

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++;
}
17 голосов
/ 27 апреля 2010

Короче говоря, язык не предлагает чистого способа сделать это. Перечисление было разработано так, чтобы оно выполнялось по одному перечислению за раз. Вы можете имитировать то, что foreach делает для вас довольно легко:

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

Что делать, если они разной длины, решать вам. Возможно, выясните, у кого еще есть элементы после выполнения цикла while, и продолжайте работать с ним, сгенерируйте исключение, если они должны быть одинаковой длины, и т. Д.

4 голосов
/ 27 апреля 2010

В .NET 4 вы можете использовать метод расширения .Zip для 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
}

Однако он не будет разбрасываться на неравные длины. Вы всегда можете это проверить.

3 голосов
/ 27 апреля 2010

Перейдите с IEnumerable.GetEnumerator, чтобы вы могли перемещаться по перечисляемому. Обратите внимание, что это может иметь очень неприятное поведение, и вы должны быть осторожны. Если вы хотите, чтобы это работало, продолжайте с этим, если вы хотите иметь поддерживаемый код, используйте два foreach.

Вы можете создать класс-оболочку или использовать библиотеку (как предлагает Джон Скит), чтобы обрабатывать эту функциональность более общим способом, если вы собираетесь использовать ее более одного раза через свой код.

Код для того, что я предлагаю:

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(); }
2 голосов
/ 27 апреля 2010
using(var enum1 = list1.GetEnumerator())
using(var enum2 = list2.GetEnumerator())
{
    while(true)
    {
        bool moveNext1 = enum1.MoveNext();
        bool moveNext2 = enum2.MoveNext();
        if (moveNext1 != moveNext2)
            throw new InvalidOperationException();
        if (!moveNext1)
            break;
        var a = enum1.Current;
        var b = enum2.Current;
        // use a and b
    }
}
1 голос
/ 27 апреля 2010

Используйте функцию Zip как

foreach (var entry in list1.Zip(list2, (a,b)=>new {First=a, Second=b}) {
    // use entry.First und entry.Second
}

Это не исключение, хотя ...

1 голос
/ 27 апреля 2010

Вы можете сделать что-то вроде этого.

IEnumerator enuma = a.GetEnumerator();
IEnumerator enumb = b.GetEnumerator();
while (enuma.MoveNext() && enumb.MoveNext())
{
    string vala = enuma.Current as string;
    string valb = enumb.Current as string;
}

C # не имеет foreach, который может делать это так, как вы хотите (что я знаю).

...