免责声明:这篇文章不再类似于我原来的答案,而是结合了我从那以后获得的大约七年的经验。我进行了编辑,因为这是一个备受关注的问题,并且现有的答案都没有真正涵盖所有角度。如果您想查看我的原始答案,可以在此帖子的修订历史中找到。
首先要了解的是 C# linq 操作,如Select()
、All()
、Where()
等,其根源在于函数式编程。这个想法是将函数式编程的一些更有用和更容易理解的部分带到 .Net 世界。这很重要,因为函数式编程的一个关键原则是操作没有副作用。很难低估这一点。但是,在ForEach()
/的情况下each()
,副作用是操作的全部目的。添加each()
orForEach()
不仅超出了其他 linq 运算符的函数式编程范围,而且与它们直接相反。
但我知道这感觉不令人满意。它可能有助于解释为什么ForEach()
从框架中省略,但无法解决手头的真正问题。你有一个真正的问题需要解决。为什么所有这些象牙塔哲学都会妨碍一些可能真正有用的东西?
当时在 C# 设计团队工作的 Eric Lippert 可以在这里为我们提供帮助。他建议使用传统的foreach
循环:
[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
关键字与循环中的操作分开。它还在定义循环操作顺序的操作之前列出循环命令。让这些操作先出现感觉更自然,然后在查询定义的末尾使用循环命令。此外,代码很丑陋。似乎能够这样写会更好:
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()
扩展方法的两个特征:
- 它不是可组合的。如果不创建新语句,我无法在与其余代码内联的循环之后添加进一步的
.Where()
或GroupBy()
或。OrderBy()
foreach
- 这不是懒惰。这些操作立即发生。例如,它不允许我有一个表单,其中用户选择一个操作作为大屏幕中的一个字段,直到用户按下命令按钮才起作用。这种形式可能允许用户在执行命令之前改变主意。对于 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()
。您并不像您想象的那样需要它,它可能会对性能和内存使用产生重大负面影响。