免责声明:这篇文章不再像我原来的答案,而是结合了我从那以后获得的大约七年的经验。我进行了编辑,因为这是一个备受关注的问题,并且现有的答案都没有真正涵盖所有角度。如果您想查看我的原始答案,可以在revision history for this post 中找到。
这里首先要了解的是 C# linq 操作,如 Select()、All()、Where() 等,其根源在于 functional programming。这个想法是将函数式编程的一些更有用和更容易理解的部分带到 .Net 世界。这很重要,因为函数式编程的一个关键原则是操作没有副作用。很难低估这一点。但是,在ForEach()/each() 的情况下,副作用是操作的全部目的。添加each() 或ForEach() 不仅超出了其他linq 运算符的函数式编程范围,而且与它们直接相反。
但我知道这让人不满意。这可能有助于解释为什么框架中省略了ForEach(),但未能解决手头的真正问题。你有一个真正的问题需要解决。为什么所有这些象牙塔哲学都会妨碍一些可能真正有用的东西?
当时在 C# 设计团队工作的 Eric Lippert 可以在这里为我们提供帮助。 He recommends using a traditional foreach loop:
[ForEach()] 为语言添加了零新的表示能力。这样做可以让你重写这段非常清晰的代码:
foreach(Foo foo in foos){ statement involving foo; }
进入这段代码:
foos.ForEach(foo=>{ statement involving foo; });
他的观点是,当您仔细查看语法选项时,您不会从 ForEach() 扩展与传统的 foreach 循环中获得任何新的东西。我部分不同意。想象一下你有这个:
foreach(var item in Some.Long(and => possibly)
.Complicated(set => ofLINQ)
.Expression(to => evaluate))
{
// now do something
}
此代码混淆了含义,因为它将foreach 关键字与循环中的操作分开。它还列出了定义循环操作顺序的操作的循环命令之前。想要先执行这些操作,然后在查询定义的end 处添加循环命令,感觉要自然得多。此外,代码很丑陋。好像能写这个会好很多:
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() 扩展方法的两个特征:
- 它不可组合。在与其余代码内联的
foreach 循环之后,我无法再添加.Where() 或GroupBy() 或OrderBy(),除非创建新语句。
- 这不是懒惰。这些操作立即发生。例如,它不允许我有一个表单,其中用户选择一个操作作为大屏幕中的一个字段,直到用户按下命令按钮才起作用。这种形式可能允许用户在执行命令之前改变主意。这对于 LINQ 查询来说是完全正常的(简单甚至),但对于
foreach 则不那么简单。
(FWIW,大多数幼稚的 .ForEach() 实现也存在这些问题。但没有它们也可以制作一个。)
当然,您可以创建自己的 ForEach() 扩展方法。其他几个答案已经实现了这种方法;这并不是那么复杂。不过,我觉得没必要。从语义和操作的角度来看,已经有一种适合我们想要做的现有方法。上述两个缺失的功能都可以通过使用现有的Select() 操作来解决。
Select() 符合上述两个示例所描述的转换或投影类型。但请记住,我仍然会避免产生副作用。对Select() 的调用应返回新对象或原始对象的投影。这有时可以通过使用匿名类型或动态对象来帮助(如果且仅在必要时)。如果您需要将结果保存在原始列表变量中,您可以随时调用.ToList() 并将其分配回原始变量。我将在这里补充一点,我更喜欢使用IEnumerable<T> 变量,而不是更具体的类型。
myList = myList.Select(item => new SomeType(item.value1, item.value2 *4)).ToList();
总结:
- 大部分时间都坚持使用
foreach。
- 如果
foreach 真的 不行(这可能不像您想象的那么频繁),请使用Select()
- 当您需要使用
Select() 时,通常仍然可以避免(程序可见的)副作用,可能通过投影到匿名类型。
- 避免打电话给
ToList()。您并不像您想象的那样需要它,它可能会对性能和内存使用产生重大负面影响。