Reputation: 2868
I've got a Dictionary
that contains a ValueTuple
:
private static readonly Dictionary<RELAY, (byte RelayNumber, bool IsClosed)> s_relays = new Dictionary<RELAY, (byte RelayNumber, bool IsClosed)>
{
{ RELAY.OUTPUT1, (4, false) },
{ RELAY.OUTPUT2, (9, false) },
{ RELAY.OUTPUT3, (11, false) },
};
Later in my code I set IsClosed
to true for one or more relays. I wrote a ForEach
extension method for the Dictionary
:
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
Debug.Assert(enumeration != null, $"Argument {nameof(enumeration)} cannot be null!");
Debug.Assert(action != null, $"Argument {nameof(action)} cannot be null!");
foreach (T item in enumeration)
{
action(item);
yield return item;
}
}
I want to call a method for each relay that is closed. For example, writing to Console.WriteLine
:
Relays.Where(x => x.Value.IsClosed).ForEach(x => Console.WriteLine(x.Key));
But this does not work. However, if I include ToList()
the code below does work:
Relays.Where(x => x.Value.IsClosed).ToList().ForEach(x => Console.WriteLine(x.Key));
What am I missing?
(I do realize that the ForEach
extension method being called is different between these two examples with the first one [mine] never being called.)
Upvotes: 3
Views: 185
Reputation: 27019
It is just a query and it will only be executed "lazily" when needed. Let's make things simple so we can see what happens:
private static readonly List<int> nums = new List<int> { 1, 2, 3 };
public static IEnumerable<int> MyEach(this IEnumerable<int> enumeration, Action<int> action)
{
foreach (var item in enumeration)
{
action(item);
yield return item;
}
}
This is just a query, so nothing will execute yet:
var query = nums.Where(x => x == 1).MyEach(x => Console.WriteLine(x));
But as soon as you do this:
query.ToList();
It will execute MyEach
extension method.
You have to be careful because it depends when it gets executed. For example, try this and you will see the number 1 printed to the console 3 times:
var query = nums.Where(x => x == 1).MyEach(x => Console.WriteLine(x));
nums.Add(1);
nums.Add(1);
query.ToList();
However, if you removed the yield
, then it will execute right away:
public static void MyEach(this IEnumerable<int> enumeration, Action<int> action)
{
foreach (var item in enumeration)
{
action(item);
//yield return item;
}
}
// Now it will execute right away
nums.Where(x => x == 1).MyEach(x => Console.WriteLine(x));
Upvotes: 1
Reputation: 33833
In this case, unlike the List<T>.ForEach()
implementation that iterates a collection, by using the yield return
pattern, your .ForEach
implementation is actually a deferred execution "filter" that will not be executed until the query is fully resolved.
In that sense it is extremely similar to the .Where()
method except that instead of reducing the query set, it is performing a side-effect operation on it as the query is resolved. Neither will actually be executed until the enumeration is enumerated.
Adding .ToList()
to the end of your query will execute your method as expected and resolve the query.
Alternatively, if you simply want a convenient way to iterate and perform an action on a collection, you could remove the yield return
and simply iterate the collection and return it at the end:
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
Debug.Assert(enumeration != null, $"Argument {nameof(enumeration)} cannot be null!");
Debug.Assert(action != null, $"Argument {nameof(action)} cannot be null!");
foreach (T item in enumeration)
{
action(item);
}
return enumeration;
}
However, the risk there is that you could potentially enumerate twice.
It is also worth noting that your second example
Relays.Where(x => x.Value.IsClosed).ToList().ForEach(x => Console.WriteLine(x.Key));
does not actually invoke your extension method. Rather, it invokes the List<T>.ForEach()
method.
Upvotes: 3