8
\$\begingroup\$

What's the best way to iterate over two collections of equal length?

Currently I'm using this:

for(var index = 0; index < collection1.Count(); index++)
{
    var item1 = collection1[index];
    var item2 = collection2[index];
    Console.WriteLine(item1.ToString() + ", "+ item2.ToString());
}

But I'd love to be able to use a foreach instead, e.g.

foreach(item1, item2 in collection1, collection2)
{
    Console.WriteLine(item1.ToString() + ", " + item2.ToString();
}

Now, this would be sort of possible if I zipped my two collections into a dictionary, but is that the best way to go? Would that be a sufficient gain of readability while sacrificing the performance needed to build the dictionary?

Similarly, I'm not sure if a dictionary is idealistically correct here, as there may not necessarily be a link between the two collections (other than their order in the list), merely a guarantee that they both have the same length.

\$\endgroup\$

4 Answers 4

12
\$\begingroup\$

You can use LINQ's Zip method. As usual, it's a bit slower than manually written code, but unless this is in a hot spot it rarely matters. The cost of Console.WriteLine vastly exceeds the cost of LINQ.

foreach(var pair in collection1.Zip(collection2, Tuple.Create))
{
    Console.WriteLine("{0}, {1}", pair.Item1, pair.Item2);
}

You can replace Tuple.Create by ValueTuple.Create if you target .net 4.7 or you referenced the compatibility nuget package. This saves a heap allocation and is usually faster.

In C# 7 you could use named tuples, which enables meaningful property names. It's based on ValueTuple and thus avoid the heap allocation as well.

names.Zip(ages, (name, age) => (Name:name, Age:age))
\$\endgroup\$
7
  • 1
    \$\begingroup\$ You could use collection1.Zip(collection2, Tuple.Create), right? \$\endgroup\$
    – Adam
    Commented May 1, 2014 at 14:55
  • \$\begingroup\$ @codesparkle Yes, that's better. Thanks for the suggestion. \$\endgroup\$ Commented May 1, 2014 at 14:58
  • \$\begingroup\$ Swapped solution to here, as result was both readable and concise. Could an anonymous type be used here to make it clearer what one was working with inside the foreach body than simply using ".Item1" and ".Item2"? \$\endgroup\$
    – Nick Udell
    Commented May 1, 2014 at 22:19
  • \$\begingroup\$ Just had time to test it, you can swap Tuple.Create with (s,s2)=> new {type1=s,type2=s2} and reference it with pair.type1 and pair.type2 and this lets you use more sensible names later on. \$\endgroup\$
    – Nick Udell
    Commented May 1, 2014 at 23:57
  • 1
    \$\begingroup\$ Note that as of .NET Core 3.0+, you can just do names.Zip(ages), and it gives you back an IEnumerable<(string, int)> \$\endgroup\$
    – canton7
    Commented Jun 25, 2022 at 10:34
3
\$\begingroup\$

The first one will always be more readable. If you want to start building collections that contain your previous collections, you'll add another layer of abstraction to your code for no reason: this will hurt readability a lot.

One of the limitations of the foreach loop is that it can only iterate one collection at a time. Working around this might seem interesting but I doubt there will be any solution that's more readable.

\$\endgroup\$
3
\$\begingroup\$

Another alternative:

using (var enumerator1 = collection1.GetEnumerator())
using (var enumerator2 = collection2.GetEnumerator()) {
    while(enumerator1.MoveNext() && enumerator2.MoveNext()) {
      Console.WriteLine(enumerator1.Current.ToString() + ", " + enumerator1.Current.ToString());
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ I think this is worse than the original code. I would use something like this only if I couldn't use indexers or Zip. \$\endgroup\$
    – svick
    Commented May 1, 2014 at 14:49
  • 1
    \$\begingroup\$ @svick this is not worse than the original code, in fact it's even better because this works with any enumerable type whereas OP's code only with ones that implement IList. \$\endgroup\$
    – t3chb0t
    Commented Jun 30, 2018 at 9:43
  • \$\begingroup\$ @t3chb0t I think the above code is harder to understand than the original, which is why I said it's worse. And I did acknowledge that it's more general in my previous comment, but I don't think that outweighs the readability issues. \$\endgroup\$
    – svick
    Commented Jun 30, 2018 at 12:26
2
\$\begingroup\$

What's the best way to iterate over two collections of equal length?

Don't. This is often (but not always) an indication that the two collections should instead be one, of some compound type.

For example, if the collections represent 2D points, with the first one containing X coordinates and the second one Y coordinates, you should instead create a new Point type and have a collection of those.

\$\endgroup\$
2
  • \$\begingroup\$ Sometimes you have to iterate over two collections of equal length when working with an existing library that you either cannot modify the source of or do not have the time to refactor. Creating a compound collection from those for your own code could work, but might be overkill if you aren't holding on to the data for very long. I do like the zipping technique people have mentioned, but that's likely due to my background in Python, where zip is a built-in function. \$\endgroup\$
    – JAB
    Commented May 1, 2014 at 15:27
  • 1
    \$\begingroup\$ @JAB I agree, zipping is a reasonable solution to this when a compound collection doesn't make sense. \$\endgroup\$
    – svick
    Commented May 1, 2014 at 15:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.