Подтвердить что ты не робот

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

Мне нужно перечислить IEnumerable<A> list1 и IEnumerable<B> list2 и пропустить их через них одновременно:

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

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

Каков наилучший способ сделать это?

4b9b3361

Ответ 1

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

Ответ 2

Вы хотите что-то вроде 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
}

Ответ 3

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

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
    }

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

Ответ 4

Перейдите с помощью 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(); }

Ответ 5

В .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
}
Тем не менее, он не будет набрасываться на неравные длины. Вы всегда можете проверить это.

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

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

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