VSInsider
Discretionary Development at Microsoft
Have you ever wondered why the Microsoft Base Class Library (BCL) lacks an IEnumerable<T>.ForEach(...) method? Obviously, Microsoft must be careful when extending any API.
The decision to add a method like IEnumerable<T>.ForEach(...) comes down to five things: demand, precedent, advantages, consistency and cost-effectiveness. Let's take a look at each.
The first question is, is there explicit customer demand for a new feature? In the case of IEnumerable<T>.ForEach(...), the answer is yes. The Microsoft Connect bug and feature inquiry site shows plenty of requests for this method, and Microsoft has begun investigating the value.
Another factor is precedent. Does the method exist elsewhere and have proven value? There's already a static implementation of ForEach() on System.Collections. Array and an instance method on System.Coll-e-ctions.Generic.List<T>. And ForEach() is in the Parallel LINQ (PLINQ) API.
Things get murky when you start talking about explicit advantages. IEnumerable<T>.ForEach(...) offers minimal advantage over a foreach statement. Compare the two examples here; one with foreach and one with IEnumerable<T>.ForEach(...), where items is an IEnumerable<string>:
items.ForEach(item=>
{Console.WriteLine(item);});
foreach (string item in items)
{ Console.Write(item); }
Yes, if you use a lambda expression (items.ForEach(Console.WriteLine)) rather than a lambda statement, the syntax is more succinct, and still readable. But for a statement block with multiple statements, it's likely that most developers would prefer the foreach statement.
The next issue is consistency, and here the outlook is less clear. Typically, the exception methods provided by System.Linq.Enumerable return a collection of objects -- generally IEnumerable<T>. This is critical because it enables deferred execution, delaying query execution until an explicit call for a result is needed. In the following example, none of the lambda expressions is evaluated until the call to Count():
items = items.Where(
item => item == item.ToString());
items = items.Where(
item => item.Length > 0);
items = items.OrderBy(item=>item);
IEnumerable<char> firstLetters =
items.Select(item => item[0]);
int count =
firstLetters.Distinct().Count();
Until the call to Count(), the criteria expressed by each lambda expression is combined together into one big query, rather than executed (or evaluated) piecemeal.
Although having IEnumerable<T>.ForEach(...) return an IEnumerable<T> collection would be consistent with the earlier, well-established pattern, it would also likely lead to runtime coding errors because statements like items.ForEach(Console.WriteLine) wouldn't write anything out. Rather, the execution of the WriteLine statement would be delayed until the expression was evaluated. Unless the result of ForEach() was assigned, it would never be evaluated, making for a line of code that appeared to be exactly what was needed but, in actuality, did nothing.
In conclusion, it's likely that consistency with other System.Linq.Enumerable methods should probably be dropped, as it was for List<T>.ForEach(). That means losing support for having a fluent API and its characteristic method chaining.
Last comes the question of cost. Implementing IEnumerable<T>.ForEach(...) is fairly trivial, as shown here:
public static void ForEach<T>(
this IEnumerable<T> collection,
Action<T> action)
{
foreach (T item in collection)
{
action(item);
}
}
However, without testing, it's likely that the consequence of deferred execution would go unnoticed. The method is small and clear enough that many would naively assume they could write it correctly without unit testing it. And, even when returning IEnumerable<T>, they would. However, once testing was in place to demonstrate that deferred execution would yield very misleading code, it would be discovered that the design needed more thought.
Also, because the method is easy to implement, it is perhaps not as crucial to include in the BCL -- a developer could implement it themselves.
IEnumerable<T>.ForEach(...) is an example of a relatively trivial API with several factors in its favor. But Microsoft must evaluate the issues very closely. We should pay similar attention to our own API design and the code we write. Will the company provide IEnumerable<T>.ForEach(...) in the Microsoft .NET Framework 5? No doubt Microsoft is considering it.
About the Author
Mark Michaelis (http://IntelliTect.com/Mark) is the founder of IntelliTect and serves as the Chief Technical Architect and Trainer. Since 1996, he has been a Microsoft MVP for C#, Visual Studio Team System, and the Windows SDK and in 2007 he was recognized as a Microsoft Regional Director. He also serves on several Microsoft software design review teams, including C#, the Connected Systems Division, and VSTS. Mark speaks at developer conferences and has written numerous articles and books - Essential C# 5.0 is his most recent. Mark holds a Bachelor of Arts in Philosophy from the University of Illinois and a Masters in Computer Science from the Illinois Institute of Technology. When not bonding with his computer, Mark is busy with his family or training for another triathlon (having completed the Ironman in 2008). Mark lives in Spokane, Washington, with his wife Elisabeth and three children, Benjamin, Hanna and Abigail.