How to iterate through two IEnumerables simultaneously?
Solution 1:
You want something like the Zip
LINQ operator - but the version in .NET 4 always just truncates when either sequence finishes.
The MoreLINQ implementation has an EquiZip
method which will throw an InvalidOperationException
instead.
var zipped = list1.EquiZip(list2, (a, b) => new { a, b });
foreach (var element in zipped)
{
// use element.a and element.b
}
Solution 2:
Here's an implementation of this operation, typically called 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);
}
}
}
}
To make it throw an exception if just one of the sequences run out of values, change the while-loop so:
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++;
}
Solution 3:
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.