I found myself wanting to use Linq to map void methods to an IEnumerable to modify all items. The existing Linq methods require a return variable when mapping, since they are based on Func
.
So I decided to try my hand at creating some extension methods myself. This is the first time I've done anything like this, so if there are any pitfalls I'm missing, please do tell.
I created two extensions, one which applies a foreach loop to all elements, calling an Action<T>
for each. The second one is basically the Zip
extension, which allows for two IEnumerable
s to be iterated together and again maps a Action<T1,T2>
to both of them.
public static void ForEachAction<T>(this IEnumerable<T> sequence, Action<T> action) {
foreach(T value in sequence) {
action(value);
}
}
public static void ForEachActionZip<Tbase, Tsecond>(this IEnumerable<Tbase> sequence, IEnumerable<Tsecond> second, Action<Tbase, Tsecond> action) {
sequence.Zip(second, (first, other) => new { first, other }).ForEachAction(x => action(x.first, x.other));
}
You can then use this like so:
someList.ForEachAction(x => x.Update());
or
someList.ForEachActionZip(secondList, (a, b) => a.Update(b));
2 Answers 2
This is generally considered to be a bad idea. IEnumerable
has deferred execution, which means that in your example, someList
can be forced to enumerate any desired number of times.
As a consequence, it depends on the implementation behind the IEnumerable
whether any changes applied by action
will still be visible after your methods have run. There are two possibilities:
someList
is a materialized list (for example,List<SomeObject>
). After runningForEachAction
, a new enumeration ofsomeList
will produce the same, modified, objects.someList
is an enumerable that produces new objects on each execution. After runningForEachAction
, a new enumeration ofsomeList
will produce new objects. The changed objects are out of scope and will soon be garbage collected.
An example of the second option is an IQueryable
against a SQL backend. When it is executed it will emit a SQL query that returns new objects from the database (caching as applied by many ORMs aside).
In that case it totally depends on what happens in action
whether any effect of it is persistent. If action
only modifies objects in someList
its effect will be lost. If it uses objects in someList
to change some external state (e.g. increment some sum value) its effect will persist.
As a conclusion, I wouldn't do this. If you want to apply void methods to any IEnumerable
, first materialize it to a list and then apply existing methods. Instead of...
someList.ForEachAction(x => x.Update());
...you'd have...
var concreteList = someList.ToList();
concreteList.ForEach(x => x.Update());
Now continue working with concreteList
so you'll be sure that the effect of x.Update()
will remain visible in your code.
-
\$\begingroup\$ I think this will only be usable for reference types. For value types the items in concreteList will still remain unchanged. \$\endgroup\$user73941– user739412018年03月23日 15:17:06 +00:00Commented Mar 23, 2018 at 15:17
-
\$\begingroup\$ @HenrikHansen But they probably won't try to modify value types. Anyway, as I said, this approach makes the effect dependent of what happens in the action, making it unpredictable as a whole. \$\endgroup\$Gert Arnold– Gert Arnold2018年03月23日 15:25:48 +00:00Commented Mar 23, 2018 at 15:25
-
\$\begingroup\$ Your point in point 1 is that if I have say two expressions with the same enumerable, with the second one having side-effects like this, the first one could be deferred until after the second one executed, causing it to enumerate with the modified objects. Is that correct? If so, is that truly different with the
.ForEach
method? \$\endgroup\$JAD– JAD2018年03月23日 20:00:59 +00:00Commented Mar 23, 2018 at 20:00 -
\$\begingroup\$ I'm not sure if I understand what you're asking, but the power of LINQ operations is that they can be chained without ever being executed. Only if in the end execution is forced by an immediately executing operation --like
ToList
,First
, orSum
-- will the whole pipeline be enumerated. The whole point of my answer is: if you change state of objects produced by anyIEnumerable
(chained or not) you must make sure that you store these objects locally. \$\endgroup\$Gert Arnold– Gert Arnold2018年03月24日 10:22:56 +00:00Commented Mar 24, 2018 at 10:22 -
\$\begingroup\$ @GertArnold But the intention for these methods is specifically to act as those immediately executing operations. They return void after all. As far as I am aware, the
foreach
loop inForEachAction
should force the enumerable to enumerate right? \$\endgroup\$JAD– JAD2018年03月28日 20:46:21 +00:00Commented Mar 28, 2018 at 20:46
If you want them to act a bit more like the Microsoft-supplied LINQ extension methods, you may want to consider some parameter checks (for example):
public static void ForEachAction<T>(this IEnumerable<T> sequence, Action<T> action) {
if (sequence== null) {
throw new ArgumentNullException(nameof(sequence));
}
if (action== null) {
throw new ArgumentNullException(nameof(action));
}
sequence.ForEachActionInternal(action);
}
private static void ForEachActionInternal<T>(this IEnumerable<T> sequence, Action<T> action) {
foreach(T value in sequence) {
action(value);
}
}
I've split it into two methods in case you decide to have your methods return a sequence of some sort lazily.
foreach
loop instead? \$\endgroup\$foreach (var item in first.Zip(second, ...))
, where...
could produce a (value) tuple or an object of an existing or anonymous type? If used a lot, you could write aZip
extension method that doesn't require aFunc<>
argument. If it returned a value tuple, you could writeforeach (var (item1, item2) in first.Zip(second))
. \$\endgroup\$