web-dev-qa-db-ja.com

2つのIEnumerableを同時に反復する方法は?

私は2つの列挙型を持っています:IEnumerable<A> list1およびIEnumerable<B> list2。私はそれらを同時に繰り返したいです:

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

同じ数の要素が含まれていない場合は、例外がスローされます。

これを行う最良の方法は何ですか?

58
Danvil

これは、通常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);
            }
        }
    }
}

シーケンスの1つだけが値を使い果たした場合に例外をスローするには、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++;
}

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
}
50
Jon Skeet

要するに、言語はこれを行うための明確な方法を提供していません。列挙は、一度に1つの列挙可能オブジェクトに対して実行されるように設計されました。あなたは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
Matt Greer

.NET 4では、IEnumerable<T>で.Zip拡張メソッドを使用できます

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
Anthony Pegram

IEnumerable.GetEnumeratorを使用して、列挙型を移動できるようにします。これは実際に厄介な動作をする可能性があるため、注意する必要があります。これを機能させたい場合はこれを使用し、保守可能なコードが必要な場合は2つのforeachを使用します。

コードで複数回使用する場合は、ラッピングクラスを作成するか、ライブラリ(Jon Skeetが示唆する)を使用して、この機能をより一般的な方法で処理できます。

私が提案するもののコード:

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

このようなことができます。

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
Jason Webb

次のようにZip関数を使用します

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

ただし、これは例外をスローしません...

1
MartinStettner