6

Given a bunch of lists, I need to iterate over them simultaneously. Suppose I have three of them: list1, list2, and list3.

What I found so far is the following:

foreach (var tuple in list1.Zip(list2, (first, second) => new { object1 = first, object2 = second })
                          .Zip(list3, (first, second) => new { object1 = first.object1, object2 = first.object2, object3 = second }))
{
  //do stuff
}

This works fine and is quite readable, unless the number of lists is not big. I know how to extend it further to 4, 5,.... lists, but if I zip 10 of them, the code would be extremely long. Is there any possibility to refactor it? Or would I need other solution than Zip function?

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
katta
  • 87
  • 1
  • 7

4 Answers4

9

With a help of a bit of code generation (think T4), one could produce up to 6 overloads (because Tuple is limited to 7 generic arguments) of something similar to:

public static class Iterate
{
    public static IEnumerable<Tuple<T1, T2, T3>> Over<T1, T2, T3>(IEnumerable<T1> t1s, IEnumerable<T2> t2s, IEnumerable<T3> t3s)
    {
        using(var it1s = t1s.GetEnumerator())
        using(var it2s = t2s.GetEnumerator())
        using(var it3s = t3s.GetEnumerator())
        {
            while(it1s.MoveNext() && it2s.MoveNext() && it3s.MoveNext())
                yield return Tuple.Create(it1s.Current, it2s.Current, it3s.Current);
        }
    }
}

With this Iterate class, iteration becomes very simple:

foreach(var t in Iterate.Over(
    new[] { 1, 2, 3 }, 
    new[] { "a", "b", "c" }, 
    new[] { 1f, 2f, 3f }))
{
}

This can be futher generalized (with a total loss of type safety) to:

public static IEnumerable<object[]> Over(params IEnumerable[] enumerables)
Anton Gogolev
  • 113,561
  • 39
  • 200
  • 288
  • Does this works same as `enum.Zip` when lists don't have the same number of elements? I don't think so. – B0Andrew Dec 22 '15 at 12:05
  • @B0Andrew Yes, iteration stops as soon as the shortest collection is exhausted. – Anton Gogolev Dec 22 '15 at 12:06
  • 1
    Well, is this just me or this answer seems over complicated ? (however a good one) – Fabjan Dec 22 '15 at 12:10
  • 1
    @Fabjan It's not complicated. It's exotic :) – Anton Gogolev Dec 22 '15 at 12:11
  • @AntonGogolev I was writing the type-unsafe version, but since you added the remark, I'll cancel :-) – Jcl Dec 22 '15 at 12:15
  • You could still keep your generalized one generic and instead return `IEnumerable>`. – krillgar Dec 22 '15 at 12:37
  • @krillgar but `T` would be unknown if the lists are not of the same type – Jcl Dec 22 '15 at 12:40
  • In 99.9% of all the cases, they should be something related, or else iterating through them all to do a similar process on them would be pointless. If you just mean that `T` is not defined, of course the method would need to be declared as `Over`. – krillgar Dec 22 '15 at 12:59
  • @krillgar I can see many cases where this is not the case. You may want to iterate over lists of different types. If you don't want the type-safety, you can just use `object`, but then using generics is pretty limiting and needs different signatures for each amount of different lists, thus the last remark (and my answer) – Jcl Dec 22 '15 at 13:11
  • I like the solution, 7 can be not enough in some cases, but after reconsidering it suits me. Thanks! – katta Dec 22 '15 at 13:18
  • A possible improvement is making the first parameter a `this`, such that it is an extension method as well (making the use more compact). Only an idea. – Willem Van Onsem Dec 22 '15 at 14:43
  • This is the approach I would take, with a single change. Pass in a func, `Func selector` and let the user decide how they want their output to be formed. This gets away from the restriction of Tuples, and allows you to use anonymous types. `Iterate.Over(xs, ys, zs, (x, y, z) => new { X = x, Y = y, Z = z })`. This is similar to how the `Zip` function already works. – KChaloux Dec 22 '15 at 17:19
4

Why not good old for loop?

  int n = new int[] {
    list1.Count,
    list2.Count,
    list3.Count,
    // etc.
  }.Min(); // if lists have different number of items

  for (int i = 0; i < n; ++i) {
    var item1 = list1[i]; // if you want an item
    ...
  }
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
1

As far as I get it, the real problem is the unknown number of lists to iterate over. Another issue I see is that there is no guarantee that all the lists will have the same length... correct?

If the number of lists is unknown, tuples won't do it because they will go up to 8... and must be set at compile time...

In that case i would suggest that you, instead of mapping to a tuple, do it to a simple and very old structure: a matrix! The width will be the number of list (known at runtime) and the depth will be the longest list. You can iterate using a simple and well know for, have the compiler optimise memory and allocation... The code will be very readable not only by C# folks but for practically anyone who works with any kind of programming language...

Leonardo
  • 10,737
  • 10
  • 62
  • 155
  • Haven't thought of this :) but wouldn't it take too much memory? sorry if that's a dumb question, I'm a noob :) – katta Dec 22 '15 at 13:17
0

Adding to @AntonGogolev's answer, on his last remark... if you don't care about type-safety and performance (for boxing-unboxing), you could implement an enumerator using object[]:

public static class Iterator
{
   public static IEnumerable<object[]> Enumerate(params IEnumerable[] enumerables)
   {
     var list = new List<object>();
     var enumerators = new List<IEnumerator>();
     bool end = false;

     foreach(var enu in enumerables)
     {
       enumerators.Add(enu.GetEnumerator());
     }
     while(!end)
     {
       list.Clear();
       foreach(var enu in enumerators)
       {
           if(!enu.MoveNext()) { end = true; break; }
           list.Add(enu.Current);                  
       }
       if(!end) yield return list.ToArray();           
     }
   }
}

Warning: no effort whatsoever has been made to optimize this code and it has been written as it came through the fingers :-)

You can use it like:

var listA = new[] { 1, 2, 3 };
var listB = new[] { "a", "b", "c" };
var listC = new[] { 5f, 6f, 7f };
foreach(var n in Iterator.Enumerate(listA, listB, listC))
{
    foreach(var obj in n)
    {
        Console.Write(obj.ToString() + ", ");
    }
    Console.WriteLine();
}

Fiddle here: https://dotnetfiddle.net/irTY8M

Jcl
  • 27,696
  • 5
  • 61
  • 92