70

I have two enumerables: IEnumerable<A> list1 and IEnumerable<B> list2. I would like to iterate through them simultaneously like:

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

If they don't contain the same number of elements, an exception should be thrown.

What is the best way to do this?

sɐunıɔןɐqɐp
  • 3,332
  • 15
  • 36
  • 40
Danvil
  • 22,240
  • 19
  • 65
  • 88
  • Answers for Java, too, please. – Thilo Apr 27 '10 at 14:22
  • Duplicate of http://stackoverflow.com/questions/981867/is-it-possible-to-iterate-over-two-ienumerable-objects-at-the-same-time ? – Jean Azzopardi Apr 27 '10 at 14:26
  • 5
    @Thilo - ask a separate question for that as the answer is likely to be very different. Don't forget to search first in case it's already been asked. – ChrisF Apr 27 '10 at 14:27
  • Why not just compare the counts to throw the exception, then otherwise handle it with a for loop and counter? it's the easiest way to enforce using a[counter] with b[counter] and knowing that your code does what you intend. "Law of unintended consequences" ;) – jcolebrand Apr 27 '10 at 14:27
  • 4
    @drachenstern You can't use a for loop or know the count of an IEnumerable (at least, not without first traversing the whole thing), as it's not a list. It's possible his enumerables are expensive to enumerate, so he just wants to do it once. – Matt Greer Apr 27 '10 at 14:32
  • @Jean: This is not a duplicate. It is neither about VB nor about List. – Danvil Apr 27 '10 at 14:32
  • @Danvil - the VB and C# solutions will be equivalent, and `List` implements `IEnumerable` (http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx) so the same answer will apply. – ChrisF Apr 27 '10 at 14:51
  • 3
    @ChrisF - Since List implements IEnumerable, if the OP had asked for a List solution, and an IEnumerable solution were supplied, then that would be applicable. However, the converse is not true. IEnumerable (as in the OP) does not implement List. – Jeffrey L Whitledge Apr 27 '10 at 14:53
  • @Matt - doh, had List on me mind... where's my caffeine? – jcolebrand Apr 27 '10 at 14:58
  • @Jeffrey - You're right. My mistake. I was pointing out that the VB and C# solutions would be equivalent and got carried away. – ChrisF Apr 27 '10 at 15:04

9 Answers9

57

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
}
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
37

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++;
}
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • I've never heard of this Zip algorithm used before, what type of problem does this solve? Seeing as it's directly supported in 4.0 clearly it's a very well known problem even if I've never had a need for it myself. – Chris Marisic Apr 27 '10 at 15:56
  • It allows you to enumerate over two collections side-by-side. You can each chain them, `a.Zip(b).Zip(c)`, but it'll give you x.Value1.Value1, x.Value1.Value2 and x.Value2. Think of the name, a "zipper", like in pants. – Lasse V. Karlsen Apr 27 '10 at 18:58
22

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.

Samuel
  • 6,126
  • 35
  • 70
Matt Greer
  • 60,826
  • 17
  • 123
  • 123
7

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.

Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
3

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(); }
jpabluz
  • 1,200
  • 6
  • 16
  • 1
    I think this contains a subtle bug. If firstEnum contains one more item than secondEnum, then no exception will be thrown. – Jeffrey L Whitledge Apr 27 '10 at 14:39
  • 5
    Don't forget to dispose the enumerators. And NEVER EVER EVER call Reset. In most enumerators it throws an exception. Reset was included for compatibility with COM enumerators. – Eric Lippert Apr 27 '10 at 15:01
  • 1
    Note that you have to say "if (first is IDisposable) ((IDisposable)first).Dispose(); – Eric Lippert Apr 27 '10 at 19:52
  • By changing "&&" to "&" it will now fail to throw an error if either collection contains one more item than the other. The problem is that the return value of MoveNext is being used in two places to do two things, but each call to MoveNext potentially changes its value. This is why the other solutions cache the value returned from MoveNext, even though it looks like a wordy, useless temp variable. – Jeffrey L Whitledge Apr 28 '10 at 13:16
  • Changed, @Eric I don't think casting is necessary. – jpabluz Apr 28 '10 at 15:58
  • @jpabluz: Well, I think it is necessary in the following circumstances: (1) the compile-time type of GetEnumerator is IEnumerator, (2) the compile-time type of GetEnumerator is a type which does not implement IDisposable, or (3) the compile-time type of GetEnumerator is a type which explicitly implements IDisposable. – Eric Lippert Apr 28 '10 at 16:20
  • 2
    @jpabluz: first: you would rather *start the compiler again at runtime* than insert a cast? Note that by making those dynamic you have made *every single operation in this program fragment dynamic also.* Second **that doesn't work either**. Dynamic will not dispatch explicit implementations. I've described how to write the code correctly several times now; why are you resisting doing it correctly? – Eric Lippert Apr 28 '10 at 21:14
  • Every change I do, is making me more knowledgeable, which its why I am here in the first place. So I have to thank you. =) – jpabluz Apr 28 '10 at 21:29
3
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
    }
}
Jeffrey L Whitledge
  • 58,241
  • 9
  • 71
  • 99
2

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

MartinStettner
  • 28,719
  • 15
  • 79
  • 106
1

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).

Jason Webb
  • 7,938
  • 9
  • 40
  • 49
  • 2
    Don't forget to dispose the enumerators. – Eric Lippert Apr 27 '10 at 15:01
  • IEnumerator is not disposable. You are thinking of the generic version IEnumerator which is disposable. The above is valid. – Jason Webb Apr 27 '10 at 15:44
  • 2
    But 99% of the `IEnumerator` instances you encounter in the wild are going to be `IEnumerator` instances with `IEnumerator` implemented for backwards compatibility. The use case the OP is proposing seems to call for lazily computed `IEnumerable` instances, so the code should call the `.Dispose()` methods of the objects returned by `a.GetEnumerator()` and `b.GetEnumerator()` if they actually implement `IDisposable`. Or just do it with generics, so you can just use a `using` block and get type-safety as a bonus. – Daniel Pryden Apr 27 '10 at 16:05
  • 1
    Exactly. The necessity of disposing an object that implements IDisposable does not magically disappear when you look at it as an IEnumerator! – Eric Lippert Apr 27 '10 at 19:51
0

Since C# 7.0 introduced tuples, you can create a generic Lockstep function that returns an IEnumerable<(T1, T2)>:

public static IEnumerable<(T1, T2)> Lockstep<T1, T2>(IEnumerable<T1> t1s, IEnumerable<T2> t2s)
{
    using IEnumerator<T1> enum1 = t1s.GetEnumerator();
    using IEnumerator<T2> enum2 = t2s.GetEnumerator();
    while (enum1.MoveNext() && enum2.MoveNext())
        yield return (enum1.Current, enum2.Current);
}

And use it like this:

void LockstepDemo(IEnumerable<A> xs, IEnumerable<B> ys)
{
    foreach (var (x, y) in Lockstep(xs, ys))
        Consume(x, y);
}

This can be easily extended to allow for three or more enumerations.

Quirin F. Schroll
  • 1,302
  • 1
  • 11
  • 25