programing

두 개의 IEnumerable을 동시에 반복하는 방법은 무엇입니까?

goodcopy 2021. 1. 18. 22:06
반응형

두 개의 IEnumerable을 동시에 반복하는 방법은 무엇입니까?


두 개의 열거 형 : IEnumerable<A> list1IEnumerable<B> list2. 다음과 같이 동시에 반복하고 싶습니다.

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

동일한 수의 요소가 포함되지 않은 경우 예외가 발생해야합니다.

이를 수행하는 가장 좋은 방법은 무엇입니까?


일반적으로 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++;
}

ZipLINQ 연산자 와 같은 것을 원 하지만 .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
}

In short, the language offers no clean way to do this. Enumeration was designed to be done over one enumerable at a time. You can mimic what foreach does for you pretty easily:

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
    }

What to do if they are of different length is up to you. Perhaps find out which one still has elements after the while loop is done and keep working with that one, throw an exception if they should be the same length, etc.


In .NET 4, you can use the .Zip extension method on 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
}

It won't throw on unequal lengths, however. You can always test that, though.


Go with IEnumerable.GetEnumerator, so you can move around the enumerable. Note that this might have some really nasty behavior, and you must be careful. If you want to get it working, go with this, if you want to have maintainable code, use two foreach.

You could create a wrapping class or use a library (as Jon Skeet suggests) to handle this functionality in a more generic way if you are going to use it more than once thru your code.

The code for what I suggest:

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

Use the Zip function like

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

This doesn't throw an exception, though ...


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

You can do something like this.

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# has no foreach that can do it how you want (that I am aware of).

ReferenceURL : https://stackoverflow.com/questions/2721939/how-to-iterate-through-two-ienumerables-simultaneously

반응형