Как выполнить итерации через два IEnumerables одновременно?

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

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

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

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

53
задан sɐunıɔןɐqɐp 28 April 2018 в 09:30
поделиться

8 ответов

Вот реализация этой операции, обычно называемой 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++;
}
32
ответ дан 7 November 2019 в 08:33
поделиться

Вы можете сделать что-то подобное.

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, который мог бы делать это так, как вы хотите (о чем я знаю).

1
ответ дан 7 November 2019 в 08:33
поделиться

В .NET 4 вы можете использовать метод расширения .Zip для IEnumerable

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
}

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

5
ответ дан 7 November 2019 в 08:33
поделиться

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

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

Это не вызывает исключения, хотя ...

{{1 }}
2
ответ дан 7 November 2019 в 08:33
поделиться

Короче говоря, язык не предлагает чистого способа сделать это. Перечисление было разработано таким образом, чтобы выполнять перечисление по одному за раз. Вы можете довольно легко имитировать то, что делает для вас 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, и продолжайте работать с этим, выбросите исключение, если они должны быть одинаковой длины и т. Д.

17
ответ дан 7 November 2019 в 08:33
поделиться

Используйте 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(); }
3
ответ дан 7 November 2019 в 08:33
поделиться

Вы хотите что-то вроде оператора LINQ Zip , но версия в .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
}
50
ответ дан 7 November 2019 в 08:33
поделиться
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
    }
}
2
ответ дан 7 November 2019 в 08:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: