Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using LINQ's Zip with a closure that doesn't return a value

Tags:

c#

linq

func

Disclaimer: this question is driven by my personal curiosity more than an actual need to accomplish something. So my example is going to be contrived. Nevertheless I think it's an issue that might very well crop up.

Let's say we are using Zip to iterate over two sequences, invoking a void method that just throws an exception if one item of the couple is found to be different from the other (therefore discarding any return value). The point here is not that the method throws an exception, so much as it returns void.

In other words, we're kind of doing a ForEach over two collections (and by the way, I know what Eric Lippert thinks about ForEach, and fully agree with him and never use it).

Now, Zip wants a Func<TFirst, TSecond, TResult>, so of course passing something equivalent to Action<TFirst, TSecond> won't work.

My question is: is there an idiomatic way that is better than this (i.e. returning a dummy value)?

var collection1 = new List<int>() { ... };
var collection2 = new List<int>() { ... };

collection1.Zip(collection2, (first, second) => 
{
    VoidMethodThatThrows(first, second);
    return true;
});
like image 458
s.m. Avatar asked Jun 25 '12 13:06

s.m.


2 Answers

I often need to execute an action on each pair in two collections. The Zip method is not useful in this case.

This extension method ForPair can be used:

public static void ForPair<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second,
    Action<TFirst, TSecond> action)
{
    using (var enumFirst = first.GetEnumerator())
    using (var enumSecond = second.GetEnumerator())
    {
        while (enumFirst.MoveNext() && enumSecond.MoveNext())
        {
            action(enumFirst.Current, enumSecond.Current);
        }
    }
}

So for your example, you could write:

var collection1 = new List<int>() { 1, 2 };
var collection2 = new List<int>() { 3, 4 };

collection1.ForPair(collection2, VoidMethodThatThrows);
like image 172
Hakakou Avatar answered Nov 07 '22 14:11

Hakakou


Use Zip() to throw the items into an object, then do your foreach however way you choose (do a normal foreach loop please, not the bad ToList/ForEach combo).

var items = collection1.Zip(collection2, (x, y) => new { First = x, Second = y });
foreach (var item in items)
{
    VoidMethodThatThrows(item.First, item.Second);
}

As of C# 7.0, improved tuple support and deconstruction makes it far more pleasing to work with.

var items = collection1.Zip(collection2, (x, y) => (x, y));
// or collection1.Zip(collection2, ValueTuple.Create);
foreach (var (first, second) in items)
{
    VoidMethodThatThrows(first, second);
}

Furthermore, .NET Core and 5 adds an overload which automatically pairs the values into tuples so you don't have to do that mapping.

var items = collection1.Zip(collection2); // IEnumerable<(Type1, Type2)>

.NET 6 adds a third collection to the mix.

var items = collection1.Zip(collection2, collection3); // IEnumerable<(Type1, Type2, Type3)>
like image 23
Jeff Mercado Avatar answered Nov 07 '22 14:11

Jeff Mercado