为什么IEnumerable没有ForEach扩展方法?

474

受另一个关于缺少Zip函数的问题启发:

为什么IEnumerable接口上没有ForEach扩展方法,或者其他地方也没有呢?唯一拥有ForEach方法的类是List<>。是否有某些原因导致它缺失,比如性能问题?


很可能就像之前的帖子所暗示的那样,在C#和VB.NET(以及许多其他语言)中,foreach是作为一种语言关键字支持的。大多数人(我自己也包括在内)只会根据需要和适当情况编写自己的foreach。 - chrisb
2
相关问题在此,附上官方答案链接:https://dev59.com/_nRA5IYBdhLWcg3wsgLq - Benjol
1
请参见https://dev59.com/GHVC5IYBdhLWcg3wtzyt。 - goodeye
1
我认为一个非常重要的点是,foreach循环更容易被发现。如果你使用.ForEach(...),当你浏览代码时很容易错过这个点。当你遇到性能问题时,这一点变得很重要。当然,.ToList()和.ToArray()也有同样的问题,但它们的使用方式略有不同。另一个原因可能是,在.ForEach中更常见的是修改源列表(例如删除/添加元素),所以它将不再是一个“纯”查询。 - Daniel Bişar
当调试并行化代码时,我经常尝试将Parallel.ForEach换成Enumerable.ForEach,只发现后者根本不存在。在这里,C#错过了一个让事情变得容易的技巧。 - Colonel Panic
显示剩余4条评论
21个回答

228

语言中已经包含了一个foreach语句,它通常可以胜任大部分工作。

我不想看到以下情况的发生:

list.ForEach( item =>
{
    item.DoSomething();
} );

改为:

foreach(Item item in list)
{
     item.DoSomething();
}

在大多数情况下,后者更清晰易读,尽管可能需要输入更长的代码。

然而,我必须承认我在这个问题上改变了我的立场; ForEach() 扩展方法在某些情况下确实是有用的。

以下是语句和方法之间的主要区别:

  • 类型检查:foreach 是在运行时进行的,ForEach() 是在编译时进行的(非常好!)
  • 调用委托的语法确实简单得多:objects.ForEach(DoSomething);
  • ForEach() 可以链接:虽然这种功能的邪恶/有用性还有待讨论。

这些都是许多人在这里提出的很好的观点,我可以理解为什么人们会想念这个函数。我不介意微软在下一个框架迭代中添加一个标准的 ForEach 方法。


50
这里的讨论给出了答案: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2386791&SiteID=1 基本上,决定保持扩展方法的功能“纯粹”。使用扩展方法时,ForEach 会鼓励产生副作用,而这并不是意图。 - mancaus
22
如果你有C#的背景,'foreach'可能更清晰易懂,但是使用内部迭代会更明确地表达你的意图。这意味着你可以像这样做:'sequence.AsParallel().ForEach(...)'。 - Jay Bazuzi
13
关于类型检查的问题,我不确定你的观点是否正确。如果可枚举对象在参数化时,foreach 在编译时进行类型检查。只有非泛型集合缺少编译时类型检查,就像一个假设的 ForEach 扩展方法一样。 - Richard Poole
60
扩展方法在使用点符号的 LINQ 链中会非常有用,例如:col.Where(...).OrderBy(...).ForEach(...)。语法更简单,可以避免使用冗余本地变量或在 foreach() 中使用冗长括号导致屏幕混乱。 - Jacek Gorgoń
48
"我不希望看到以下情况发生"-- 这个问题并非关于你的个人喜好,你通过添加多余的换行符和一对多余的大括号使代码变得更加讨厌。而不那么讨厌的写法是 list.ForEach(item => item.DoSomething())。谁说它一定是列表?显然使用场景是在链的末尾;使用 foreach 语句的话,你需要将整个链放在 in 后面,并在块内执行操作,这远不如自然和一致。 - Jim Balter
显示剩余23条评论

89

ForEach方法是在LINQ之前添加的。如果您添加了ForEach扩展,由于扩展方法的限制,它将永远不会被List实例调用。我认为没有添加它的原因是为了不干扰现有的方法。

然而,如果你真的想念这个小巧的函数,你可以自己编写一个版本。

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}

15
扩展方法可以实现这一点。如果已经存在给定方法名称的实例实现,则使用该方法而不是扩展方法。因此,您可以实现一个扩展方法 ForEach,并且不会与 List<>.ForEach 方法发生冲突。 - Cameron MacFarland
3
相信我,Cameron,我知道扩展方法是如何工作的 :) List 继承自 IEnumerable,所以这将是一个不明显的事情。 - aku
12
从“扩展方法编程指南”(http://msdn.microsoft.com/en-us/library/bb383977.aspx)中得知,与接口或类方法具有相同名称和签名的扩展方法将永远不会被调用。对我来说似乎很明显。 - Cameron MacFarland
3
我在说些不同的东西吗?我认为当许多类具有ForEach方法但其中一些可能工作方式不同时,这是不好的。 - aku
5
关于List<>.ForEach和Array.ForEach有一个有趣的事情需要注意,它们实际上并没有在内部使用foreach语句。它们都使用了普通的for循环。也许这是出于性能方面的考虑(http://diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx)。但鉴于Array和List都实现了ForEach方法,令人惊讶的是它们没有至少为IList<>实现一个扩展方法,甚至不是为IEnumerable<>。 - Matt Dotson
显示剩余4条评论

60

你可以编写这个扩展方法:

// Possibly call this "Do"
IEnumerable<T> Apply<T> (this IEnumerable<T> source, Action<T> action)
{
    foreach (var e in source)
    {
        action(e);
        yield return e;
    }
}

优点

允许链接:

MySequence
    .Apply(...)
    .Apply(...)
    .Apply(...);

缺点

除非进行强制迭代的操作,否则它实际上不会执行任何操作。因此,它不应该被称为.ForEach()。你可以在结尾处写.ToList(),或者也可以编写这个扩展方法:

// possibly call this "Realize"
IEnumerable<T> Done<T> (this IEnumerable<T> source)
{
    foreach (var e in source)
    {
        // do nothing
        ;
    }

    return source;
}

这可能与C#库的发货方式有太大的不同;那些不熟悉您的扩展方法的读者将不知道如何处理您的代码。


1
如果您这样编写您的第一个函数:{foreach (var e in source) {action(e);} return source;},则可以在不使用“realize”方法的情况下使用它。 - jjnguy
4
你不可以只使用Select方法来替代Apply方法吗?在我看来,对每个元素应用一个操作并返回它似乎正是Select所做的事情。例如:numbers.Select(n => n*2); - rasmusvhansen
1
我认为对于这个目的,使用Apply比使用Select更易读。阅读Select语句时,我会将其理解为“从内部获取某些内容”,而Apply则是“执行一些操作”。 - Dr Rob Lang
@jinguy 这个问题在于带有副作用(读取文件)的可枚举对象将无法正常工作。 - NetMage
2
@rasmusvhansen 实际上,Select() 表示对每个元素应用一个函数并返回该函数的值,而 Apply() 表示对每个元素应用一个 Action 并返回__原始__元素。 - NetMage
3
这相当于 Select(e => { action(e); return e; }) - Jim Balter

45

这里的讨论给出了答案:

实际上,我目睹的具体讨论确实是关于函数纯度的。在一个表达式中,通常会假设不存在副作用。使用ForEach会明确地引发副作用,而不仅仅是忍受它们。-- Keith Farmer (合伙人)

基本上,决定保持扩展方法的功能“纯粹”。对于可枚举的扩展方法,使用ForEach将鼓励副作用,而这并不是想要的。


7
请参考 http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx 。这么做违反了基于所有其他序列运算符的函数式编程原则。显然,调用此方法的唯一目的是引起副作用。 - Michael Freidgeim
15
那种推理完全是无稽之谈。使用情境 是需要副作用的……如果没有 ForEach,你必须编写一个 foreach 循环。存在 ForEach 不会鼓励副作用,而它的缺失也不会减少副作用。 - Jim Balter
考虑到编写扩展方法的简单性,实际上没有理由不将其作为语言标准。 - Captain Prinny
2
@MichaelFreidgeim的失效链接存档:https://web.archive.org/web/20151205151706/http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx - Vapid

22

虽然我同意在大多数情况下最好使用内置的foreach构造,但我发现使用这个变体的ForEach<>扩展比自己在常规的foreach中管理索引要更好一些:

public static int ForEach<T>(this IEnumerable<T> list, Action<int, T> action)
{
    if (action == null) throw new ArgumentNullException("action");

    var index = 0;

    foreach (var elem in list)
        action(index++, elem);

    return index;
}
抱歉,我只能使用英文回答您的问题。
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

我可以为你提供:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry

很好的扩展,但是你能加些文档吗? 返回值的预期使用是什么? 为什么索引变量不是特定的 int 类型? 示例用法? - Mark T
返回值是列表中元素的数量。由于隐式类型转换,索引*被明确定义为int类型。 - Chris Zwiryk
@Chris,根据你上面的代码,返回值是列表中最后一个值的从零开始的索引,而不是列表中元素的数量。 - Eric Smith
@Chris,不是这样的:索引在取值后被递增(后缀递增),因此在处理第一个元素后,索引将为1,依此类推。因此返回值确实是元素计数。 - MartinStettner
@MartinStettner,怎么了?这个方法应该返回元素计数。 - Chris Zwiryk
1
@MartinStettner,Eric Smith在5/16的评论是针对早期版本的。他在5/18更正了代码以返回正确的值。 - Michael Blackburn

17

一种解决方法是编写 .ToList().ForEach(x => ...)

优点:

易于理解 - 读者只需要知道C#自带的内容,而不需要了解任何其他扩展方法。

语法噪音非常小(只增加了一些不必要的代码)。

通常不会额外消耗内存,因为本地的.ForEach()本来就需要实现整个集合。

缺点:

操作顺序不理想。我更喜欢先实现一个元素,然后对其进行操作,然后重复。这段代码首先实现所有元素,然后按顺序对它们进行操作。

如果实现列表时出现异常,则无法对单个元素进行操作。

如果枚举是无限的(例如自然数),那么你没有办法。


1
a) 这根本不是问题的答案。 b) 如果一开始就提到了,虽然没有Enumerable.ForEach<T>方法,但有一个List<T>.ForEach方法,这个响应会更清楚。 c) “通常不会额外消耗内存,因为本地的 .ForEach() 方法无论如何都需要实现整个集合。” —— 完全是胡说八道。下面是 C# 提供的 Enumerable.ForEach<T> 实现:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); } - Jim Balter

17

我一直都有这个疑问,所以我一直随身携带这个:

public static void ForEach<T>(this IEnumerable<T> col, Action<T> action)
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    foreach (var item in col)
    {
        action(item);
    }
}

不错的小扩展方法。

2
Col可能为null...您也可以检查一下。 - Amy B
2
我觉得很有趣的是,每个人都会检查参数是否为空,但不检查结果也会导致异常... - oɔɯǝɹ
完全不是这样。Null引用异常并不能告诉你哪个参数出了问题。你也可以在循环中检查,这样你就可以抛出一个特定的Null引用异常,说明col[index#]为空的原因。 - Roger Willcocks
@Roger Willcocks:假设扩展方法有一定的意图,那将是不好的。null可能是可枚举对象中的一个有效元素,而该操作可以处理它:想象一下,可枚举对象是一个字符串列表,而该操作是一个修剪操作-它可以决定将null修剪为空字符串,这样一切都很好,你绝对不希望在这种情况下出现异常。 - Christoph
抱歉,我的示例很糟糕(那只适用于“选择”)。 - Christoph
显示剩余3条评论

8

最近关于 ForEach 扩展方法不适用的评论很多,因为它不像 LINQ 扩展方法那样返回一个值。虽然这是事实,但并不完全正确。

LINQ 扩展方法都返回一个值,因此它们可以链接在一起使用:

collection.Where(i => i.Name = "hello").Select(i => i.FullName);

然而,仅仅因为LINQ是使用扩展方法实现的,并不意味着扩展方法必须以相同的方式并返回一个值。编写一个公开常用功能但不返回任何值的扩展方法是完全有效的。
关于ForEach的具体争论是,根据扩展方法的限制(即扩展方法将从不覆盖具有相同签名的继承方法),可能存在这样一种情况,其中自定义扩展方法可在实现IEnumerable<T>的所有类中使用,除了List<T>。当方法开始根据调用的是扩展方法还是继承方法而表现出不同行为时,这可能会引起混淆。

7
您可以使用可链接但惰性计算的Select,首先执行您的操作,然后返回身份(或其他您喜欢的内容)。
IEnumerable<string> people = new List<string>(){"alica", "bob", "john", "pete"};
people.Select(p => { Console.WriteLine(p); return p; });

你需要确保它仍然被评估,可以使用Count()(据我所知,这是最便宜的枚举操作)或其他你需要的操作来实现。我希望看到它被引入标准库中。
static IEnumerable<T> WithLazySideEffect(this IEnumerable<T> src, Action<T> action) {
  return src.Select(i => { action(i); return i; } );
}

以上代码变为people.WithLazySideEffect(p => Console.WriteLine(p)),它实际上等同于foreach,但是是延迟和可链接的。

太棒了,我从未想过一个返回的select会像这样工作。这是方法语法的一个很好的用法,因为我认为你不能用表达式语法来做到这一点(因此我以前没有想到过这个)。 - Alex KeySmith
@AlexKeySmith 如果C#有一个类似于其前身的序列运算符,你可以使用表达式语法来完成它。但是ForEach的整个重点在于它采取了void类型的操作,而在C#中无法在表达式中使用void类型。 - Jim Balter
在我看来,现在的 Select 已经不再执行它原本应该执行的操作了。虽然它确实是解决 OP 所提出问题的一种方法,但就主题可读性而言:没有人会期望选择操作会执行一个函数。 - Matthias Burger
@MatthiasBurger 如果 Select 没有按照预期执行,那么问题出在 Select 的实现上,而不是调用 Select 的代码上,因为调用方对 Select 的执行没有任何影响。 - Martijn

6

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接