免责声明:这篇帖子已经不再与我最初的回答相似,而是结合了我过去七年的经验。我进行了编辑,因为这是一个非常受关注的问题,现有的回答并没有涵盖所有方面。如果你想看到我的原始回答,可以在此帖子的修订历史中找到链接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 =>
{
});
然而,即使在这里,我最终也接受了Eric的观点。我意识到像你上面看到的代码需要一个额外的变量来调用。如果你有一组复杂的LINQ表达式,你可以通过首先将LINQ表达式的结果赋值给一个新的
命名得当的变量来为你的代码添加有价值的信息。
var queryForSomeThing = Some.Long(and => possibly)
.Complicated(set => ofLINQ)
.Expressions(to => evaluate);
foreach(var item in queryForSomeThing)
{
}
这段代码感觉更自然。它将 foreach
关键字放回到循环的其余部分之后,并在查询定义之后。最重要的是,变量名可以添加新的信息,有助于未来的程序员理解 LINQ 查询的目的。再次说明,我们看到所期望的 ForEach()
操作符实际上没有为语言增加新的表达能力。
然而,我们仍然缺少一个假设的 ForEach()
扩展方法的两个特性:
- 它不可组合。我不能在
foreach
循环之后内联写入剩余的代码中添加进一步的 .Where()
、GroupBy()
或 OrderBy()
,而不创建新的语句。
- 它不是延迟执行的。这些操作会立即发生。它不允许我在一个更大的屏幕中的一个字段中选择一个操作,直到用户按下命令按钮之前不进行操作。这种形式可能允许用户在执行命令之前改变主意。这在 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()。你并不需要它那么多,而且它可能对性能和内存使用产生重大负面影响。