在 Enumerable<T> 中为所有元素执行特定操作

63

我有一个Enumerable<T>,我正在寻找一种允许我执行每个元素操作的方法,有点像Select,但是用于副作用。类似这样:

string[] Names = ...;
Names.each(s => Console.Writeline(s));
或者
Names.each(s => GenHTMLOutput(s));   
// (where GenHTMLOutput cannot for some reason receive the enumerable itself as a parameter)

我尝试了Select(s=> { Console.WriteLine(s); return s; }),但没有输出任何内容。


10个回答

72

一个快速且简单的方法是:

Names.ToList().ForEach(e => ...);

7
ToList()会评估IEnumerable中的每个节点,这意味着任何惰性评估都在该点完成。我更倾向于通过制作自己的扩展方法来尽可能地延迟此操作。 - Jeff Yates
3
抱歉,伙计们,我选择了不受欢迎的方法。 我知道失去惰性评估的缺点,但当我在我的列表上执行ForEach时,我希望它立即发生,所以现在懒惰已经没有太多优势了。 - Daniel Magliola
7
这个有一个很大的问题。如果你使用ToList,那么所有的对象必须在内存中一次性存在。如果源是数据库,那么使用IEnumerable至少可以在完成后卸载对象。使用ToList,在处理大型数据集时可能会耗尽内存!不要使用它! - Peter Morris
3
是的,这种方法确实有很多缺点,但我惊讶地发现它们经常不是问题,而且这种简单的方法效果也很好。事实上,我的大多数序列都小于100个元素。(也许你做的编程比我更有趣。) - Jay Bazuzi
@JeffYates 进行具有副作用的操作的整个目的在于执行发生在现在,而不是懒惰地延迟——对于懒惰的情况,已经存在Select - MicroVirus
显示剩余3条评论

31
免责声明:这篇帖子已经不再与我最初的回答相似,而是结合了我过去七年的经验。我进行了编辑,因为这是一个非常受关注的问题,现有的回答并没有涵盖所有方面。如果你想看到我的原始回答,可以在此帖子的修订历史中找到链接1。

在这里首先要理解的是C#中的linq操作,如 Select()All()Where() 等,其根源可以追溯到functional programming。这个想法是将一些更有用和易于理解的函数式编程部分引入到.NET世界中。这很重要,因为函数式编程的一个关键原则是操作应该没有副作用。这一点很难被低估。然而,在 ForEach()/each()的情况下,副作用是操作的整个目的。添加 each()ForEach() 不仅超出了其他linq运算符的函数式编程范围,而且与它们直接相悖。

但我理解这可能不令人满意。这可能有助于解释为什么框架中省略了 ForEach(),但未能解决实际问题。您需要解决一个真正的问题。为什么所有这些象牙塔哲学要妨碍那些可能真正有用的东西呢?

埃里克·利珀特(Eric Lippert)是当时的C#设计团队成员,他可以帮助我们。他建议使用传统的foreach循环:

[ForEach()]对语言没有增加任何新的表示能力。这样做可以将这段完全清晰的代码重写为:

foreach(Foo foo in foos){ 使用foo的语句; }

转换为以下代码:

foos.ForEach(foo=>{ 使用foo的语句; });

他的观点是,仔细看待语法选项时,使用ForEach()扩展与传统的foreach循环相比,并没有获得任何新的东西。我部分地不同意。想象一下你有这样的代码:

 foreach(var item in Some.Long(and => possibly)
                         .Complicated(set => ofLINQ)
                         .Expression(to => evaluate))
 {
     // now do something
 } 

这段代码混淆了意义,因为它将foreach关键字与循环中的操作分开。它还在循环操作之前列出了循环命令prior,用于定义循环操作的序列。更自然的做法是先写出这些操作,然后在查询定义的末尾放置循环命令。此外,这段代码看起来很丑陋。能够像这样编写代码会更好看一些:
Some.Long(and => possibly)
   .Complicated(set => ofLINQ)
   .Expression(to => evaluate)
   .ForEach(item => 
{
    // now do something
});

然而,即使在这里,我最终也接受了Eric的观点。我意识到像你上面看到的代码需要一个额外的变量来调用。如果你有一组复杂的LINQ表达式,你可以通过首先将LINQ表达式的结果赋值给一个新的命名得当的变量来为你的代码添加有价值的信息。
var queryForSomeThing = Some.Long(and => possibly)
                        .Complicated(set => ofLINQ)
                        .Expressions(to => evaluate);
foreach(var item in queryForSomeThing)
{
    // now do something
}

这段代码感觉更自然。它将 foreach 关键字放回到循环的其余部分之后,并在查询定义之后。最重要的是,变量名可以添加新的信息,有助于未来的程序员理解 LINQ 查询的目的。再次说明,我们看到所期望的 ForEach() 操作符实际上没有为语言增加新的表达能力。

然而,我们仍然缺少一个假设的 ForEach() 扩展方法的两个特性:

  1. 它不可组合。我不能在 foreach 循环之后内联写入剩余的代码中添加进一步的 .Where()GroupBy()OrderBy(),而不创建新的语句。
  2. 它不是延迟执行的。这些操作会立即发生。它不允许我在一个更大的屏幕中的一个字段中选择一个操作,直到用户按下命令按钮之前不进行操作。这种形式可能允许用户在执行命令之前改变主意。这在 LINQ 查询中是完全正常的(甚至是容易的),但在 foreach 中并不那么简单。
(顺便说一句,大多数天真的.ForEach()实现也存在这些问题。但是有可能通过巧妙地编写一个方法来避免这些问题。)
当然,你可以自己编写一个ForEach()扩展方法。其他答案中已经有了这个方法的实现;它并不复杂。然而,我觉得这是不必要的。已经存在一个方法,从语义和操作角度都符合我们想要做的事情。上面提到的两个缺失功能都可以通过使用现有的Select()操作来解决。
Select()适用于上述两个示例所描述的转换或投影类型。但请记住,我仍然建议避免产生副作用。调用Select()应该返回新对象或原始对象的投影。如果必要的话,可以通过使用匿名类型或动态对象来辅助实现。如果你需要将结果保留在原始列表变量中,可以随时调用.ToList()并将其重新赋值给原始变量。我还要补充一点,尽可能多地使用IEnumerable变量而不是更具体的类型进行操作。
myList = myList.Select(item => new SomeType(item.value1, item.value2 *4)).ToList();

总结一下:
1. 大部分情况下,只需使用foreach即可。 2. 当foreach真的无法满足需求时(这种情况可能比你想象的少),可以使用Select()。 3. 当需要使用Select()时,通常仍然可以避免(在程序中可见的)副作用,可能通过投影到匿名类型来实现。 4. 避免过度依赖ToList()。你并不需要它那么多,而且它可能对性能和内存使用产生重大负面影响。

1
被踩:只有在谓词继续返回“true”时,才会处理所有项目。如果您执行的是返回布尔值的方法,则很容易在不知情的情况下引入错误。 - Peter Morris
@PeterMorris 我相信他的意思是你可以使用lambda来做任何你想做的事情,当你执行完代码后,只需返回true即可。在这种情况下,你可能需要使用一个需要花括号的语句lambda。如果你只想使用内置功能并保留惰性评估,这不是一个坏的解决方案。 - Alex Jorgenson
1
@Peter Morris 绝对没错。在我写这篇文章之前,我还没有完全意识到这点。我了解linq操作符,但对它们背后的函数式编程根源则不是那么深刻的理解。今天,我同意Eric Lippert推荐只使用foreach. - Joel Coehoorn
我唯一的抱怨是在这种情况下foreach语义有点奇怪。它将循环命令放在循环操作的查询之前。ForEach()扩展将事物放在更好的顺序中:做所有这些过滤/排序等,然后在所有这些之后对剩下的任何东西执行某些操作。即使在这里,我认为这表明我应该将我的linq操作分配给某个变量,然后在foreach循环中使用此变量。然后,变量名称可以为代码添加有关某些本来复杂的linq指令旨在完成的内容的有价值信息。 - Joel Coehoorn
1
哦...foreach的另一个抱怨是它不返回序列。但是,如果你需要它,我建议使用Select()返回一个新对象(无副作用),而不是使用All()。七年可真是很长的时间 ;) - Joel Coehoorn
显示剩余4条评论

21

您正在寻找永远难以捉摸的ForEach,它目前仅存在于List泛型集合中。关于Microsoft是否应该将其作为LINQ方法添加到其中,网络上有许多讨论。目前,您必须自己编写代码:

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

All()方法提供了类似的能力,但它的使用场景是对每个项目执行谓词测试而不是动作。当然,它可以被迫执行其他任务,但这会改变语义,并使其他人更难解释您的代码(即,All()用于谓词测试还是动作?)。


1
我更喜欢添加它。包括一个重载,它需要一个Func<T,TResult>操作并返回IEnumerable<TResult>。即使这已经可以通过Select完成。 - peSHIr
如果这是您的使用情况,您肯定可以编写一个来完成这个任务。 - Jeff Yates
2
使用 All 是一个不好的主意 - 如果任何一个 Func<T, bool> 返回 false,那么执行将会结束。 - Amy B
是的,这是另一个不使用它的很好的理由。 - Jeff Yates
1
这并不是懒惰,对吧?所以它会立即评估查询。与普通的foreach相比没有真正的优势。 - kristianp
@kristianp 不,它不会立即评估。这种方法的唯一优点是,您将拥有一个扩展方法,以后可以在任何IEnumerable中使用。换句话说,这将成为一种“语法糖”,可用作快捷方式,但这正是扩展方法的目的。 - Alfredo A.

6

很遗憾,在当前版本的LINQ中没有内置的方法来实现这一点。框架团队忽略了添加.ForEach扩展方法。目前在以下博客上正在进行关于此问题的讨论。

http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

不过,添加一个相当容易。

public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action) {
  foreach ( var cur in enumerable ) {
    action(cur);
  }
}

为了能够在连接的 LinQ 序列中使用它,请考虑将可枚举对象作为 IEnumerable<T> 返回。例如:...Select(...).ForEach(...).Where(...).Distinct(); - Harald Coppoolse

5

使用LINQ和IEnumerable无法立即完成此操作 - 您需要实现自己的扩展方法或使用LINQ将枚举转换为数组,然后调用Array.ForEach():

Array.ForEach(MyCollection.ToArray(), x => x.YourMethod());

请注意,由于值类型和结构体的工作方式不同,如果集合是值类型并且您通过这种方式修改集合的元素,则不会对原始集合的元素产生任何影响。

3
因为LINQ被设计成一个查询功能而不是更新功能,所以你不会找到一个扩展来在IEnumerable<T>上执行方法,因为这可能会导致执行一个带有副作用的方法。在这种情况下,你可能只需要坚持使用foreach(string name in Names) Console.WriteLine(name)。

2

使用并行 Linq:

Names.AsParallel().ForAll(name => ...)


1

好的,你也可以使用标准的foreach关键字,只需将其格式化为单行:

foreach(var n in Names.Where(blahblah)) DoStuff(n);

抱歉,我认为这个选项应该在这里:)


0

列表中有一个ForEach方法。您可以通过调用.ToList()方法将Enumerable转换为List,然后从该列表调用ForEach方法。

或者,我听说过有人在IEnumerable上定义自己的ForEach方法。这可以通过基本上调用ForEach方法,但是将其包装在扩展方法中来实现:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> _this, Action<T> del)
    {
        List<T> list = _this.ToList();
        list.ForEach(del);
        return list;
    }
}

这将强制执行完整的序列评估,为临时列表的每个元素占用内存。只要能够避免,我会选择在IEnumerable<T>上使用扩展方法,以尽可能长时间地保留惰性求值特性。 - peSHIr

-1
如前所述,ForEach扩展将解决问题。
对于当前的问题,我的建议是如何执行迭代器。
我尝试了Select(s=> { Console.WriteLine(s); return s; }),但它没有打印任何内容。
请检查一下。
_= Names.Select(s=> { Console.WriteLine(s); return 0; }).Count();
试一试吧!

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