foreach与someList.ForEach(){}的区别

211

显然有很多方法可以迭代集合。好奇是否存在差异,或为什么您会使用一种而不是另一种方法。

第一种类型:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

另一种方式:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

我想,从我的脑海中想到的是,你可以使用一个可重用的委托来代替上面使用的匿名委托...

6
我建议阅读Eric Lippert的博客文章《foreach vs ForEach》。链接为:http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx - Erik Philips
6
@ErikPhilips提供的链接已经过时。但是,这篇文章仍然可以在这里阅读到:ericlippert.com/2009/05/18/foreach-vs-foreach - jasie
13个回答

162

这两者之间有一个重要且有用的区别。

因为.ForEach使用for循环来迭代集合,所以这是有效的(编辑:在 .net 4.5 之前 - 实现已更改,它们都会抛出异常):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

foreach使用枚举器,所以这是无效的:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

简而言之:不要将此代码复制粘贴到您的应用程序中!

这些示例并不是最佳实践,它们只是为了演示ForEach()foreach之间的区别。

for循环中从列表中删除项目可能会产生副作用。最常见的一个在这个问题的评论中有描述。

通常,如果您想从列表中删除多个项目,您需要将确定要删除哪些项目与实际删除分开。这不会使您的代码紧凑,但它保证您不会错过任何项目。


41
即使如此,你应该使用 someList.RemoveAll(x => x.RemoveMe)。 - Mark Cidade
4
使用 Linq,所有事情都可以做得更好。我只是展示了一个在 foreach 中修改集合的例子。 - user1228
2
RemoveAll() 是 List<T> 上的一个方法。 - Mark Cidade
3
也许您已经知道这一点,但是人们应该注意不要用这种方式删除项目;如果您删除了项目N,则迭代将跳过项目(N+1),您将无法在委托中看到它或有机会将其删除,就像在自己的for循环中执行此操作一样。 - El Zorko
13
如果你使用 for 循环 倒序 遍历列表,就可以在不产生索引偏移问题的情况下删除其中的元素。 - starikcetin
显示剩余8条评论

88

我们曾经有一些代码(在VS2005和C#2.0中)其中前任工程师极力使用list.ForEach(delegate(item) { foo; });而不是foreach(item in list){foo; };写他们编写的所有代码,例如从数据读取器中读取行的代码块。

我仍然不确定他们为什么这样做。

list.ForEach()的缺点是:

  • 在C#2.0中它更冗长。但是,在C#3及以后的版本中,您可以使用“=>”语法来生成一些漂亮而简洁的表达式。

  • 它不太熟悉。必须维护此代码的人会想知道为什么要这样做。我花了一段时间才决定没有任何原因,除了可能让编写者看起来聪明一点(其余代码的质量削弱了这一点)。它也不太可读,具有在委托代码块末尾的“})”。

  • 请参见Bill Wagner的书《Effective C#: 50 Specific Ways to Improve Your C#》,他谈到为什么优先使用foreach而不是其他循环,如for或while循环 - 主要的是您让编译器决定构造循环的最佳方式。如果将来的编译器版本成功使用更快的技术,则通过使用foreach并重新构建,您将获得此免费功能,而无需更改代码。

  • foreach(item in list)结构使您能够在需要退出迭代或循环时使用breakcontinue。但是您不能在foreach循环内部更改列表。

我很惊讶地发现 list.ForEach 稍微快一些。但这可能不是在整个程序中都使用它的有效理由,那样做属于过早优化。如果你的应用程序使用数据库或 Web 服务,时间几乎总会花费在这些地方,而不是循环控制上。你是否也已经对 for 循环进行了基准测试?list.ForEach 可能由于内部使用了这种方法,而没有包装器的 for 循环将更快。

我不同意 list.ForEach(delegate) 版本在任何重要的方面上都是“更函数式”的观点。虽然它确实将一个函数传递给了另一个函数,但结果或程序组织并没有什么大的区别。

我认为 foreach(item in list) 并不能“精确地表达你想要的方式”- 一个 for(int i = 0; i < count; i++) 循环可以做到这一点,而 foreach 循环则将控制的选择留给了编译器。

我的感觉是,在新项目中,为了符合通用用法和可读性,对于大多数循环都使用 foreach(item in list),仅在短代码块中使用 list.ForEach(),当你可以用 C#3 的 "=>" 运算符更优雅或简洁地完成某些操作时。在这种情况下,可能已经有比 ForEach() 更具体的 LINQ 扩展方法。看看 Where(), Select(), Any(), All(), Max() 或其他许多 LINQ 方法是否已经实现了你想要从循环中实现的功能。


只是出于好奇,看看微软的实现... http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,0e5a9cf0a310b9e5 - Alexandre

23

俗话说,细节决定成败...

集合枚举的两种方法最大的区别在于 foreach 会带有状态,而 ForEach(x => { }) 则不会。

但是让我们深入探讨一下,因为有些事情可能会影响您的决策,并且在编写任一情况下的代码时应该注意一些注意事项。

让我们在我们的小实验中使用 List<T> 来观察行为。对于这个实验,我使用的是 .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

首先我们使用 foreach 进行迭代:

foreach (var name in names)
{
    Console.WriteLine(name);
}

我们可以将其扩展为:
using (var enumerator = names.GetEnumerator())
{

}

手持枚举器,深入了解之后我们得到以下内容:
public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

两件事情立即变得明显:

  1. 我们返回了一个有关底层集合的有状态对象。
  2. 集合的副本是浅拷贝。

当然,这样做并不安全。正如上面所指出的那样,在迭代时更改集合只会带来麻烦。

但是,如果在我们迭代期间通过其他方式使集合无效,该怎么办?最佳实践建议在操作和迭代期间对集合进行版本控制,并检查版本以检测基础集合何时发生更改。

这里的问题就变得非常模糊了。根据Microsoft的文档:

如果对集合进行更改,例如添加、修改或删除元素,则枚举器的行为未定义。

那这是什么意思呢?例如,仅因为 List<T> 实现了异常处理,并不意味着所有实现 IList<T> 的集合都会这样做。这似乎是Liskov替换原则的明显违反:

超类的对象应该能够被其子类的对象替换,而不会破坏应用程序。

另一个问题是枚举器必须实现 IDisposable —— 这意味着潜在的内存泄漏来源增加了,不仅如果调用者弄错了,而且如果作者没有正确实现 Dispose 模式。

最后,我们有一个生命周期问题... 如果迭代器有效,但基础集合已经不存在怎么办?我们现在有了曾经存在的快照...当你将集合和其迭代器的生命周期分开时,你会遇到麻烦。

现在让我们来看看 ForEach(x => { })

names.ForEach(name =>
{

});

这将扩展为:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

需要注意的是以下代码:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

这段代码不会分配任何枚举器(没有需要Dispose的内容),并且在迭代时不会暂停

需要注意的是,这也执行基础集合的浅复制,但此时该集合是某个时间点的快照。如果作者没有正确实现检查集合是否改变或过时的功能,则该快照仍然有效。

这并不保护您免受生存期问题的影响...如果基础集合消失了,则现在您有一个指向已消失内容的浅复制...但至少您不必处理孤立的迭代器中的Dispose问题...

是的,我说的是迭代器...有时拥有状态是有优势的。假设您想要维护类似于数据库游标的东西...也许多个foreach风格的Iterator<T>是可行的。我个人不喜欢这种设计方式,因为存在太多的生存期问题,并且您依赖于您所依赖的集合的作者的好意(除非您真正自己从头开始编写所有内容)。

总有第三种选择...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

虽然不太吸引人,但它很有用(向汤姆·克鲁斯和电影《法律事务所》道歉)

这是你的选择,但现在你已经知道了,可以做出明智的决定。


19

为了好玩,我把列表放入反射器中,这是生成的 C# 代码:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

同样地,在foreach中所使用的Enumerator中的MoveNext如下所示:
public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach比MoveNext要精简得多,处理的内容也少得多,更可能JIT成高效的东西。

此外,无论如何foreach()都会分配一个新的枚举器。GC是你的朋友,但如果你反复做同样的foreach,这将产生更多的一次性对象,而不是重用相同的委托 - 但这真的是一个边缘情况。在典型的使用中,你几乎看不到任何区别。


1
您无法保证foreach生成的代码在编译器版本之间是相同的。未来版本可能会改进生成的代码。 - Anthony
3
截至.NET Core 3.1,ForEach仍然更快。 - jackmott

16

我认为someList.ForEach()调用可以很容易地并行化,而普通的foreach不太容易并行运行。
你可以轻松地在不同的核心上运行多个不同的委托,这对于普通的foreach来说并不容易实现。
只是我的个人看法。


2
我认为他的意思是运行时引擎可以自动并行化它。否则,foreach和.ForEach都可以通过手动在每个操作委托中使用线程池来并行化。 - Isak Savo
1
@Isak 但这将是一个不正确的假设。如果匿名方法提升了本地变量或成员,则运行时将无法轻松并行化。 - Rune FS

15

我知道两个相对不常见的事情,这样它们就有所区别。太厉害了!

首先,有一个常见错误,即为列表中的每个项目都创建一个委托。如果使用foreach关键字,则所有委托可能都会引用列表的最后一个项目:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

List.ForEach方法没有这个问题。迭代的当前项被按值作为参数传递给外部lambda,然后内部lambda正确地在其自己的闭包中捕获了该参数。问题解决了。

(不幸的是,我认为ForEach是List的成员而不是扩展方法,尽管很容易定义它自己,以便在任何可枚举类型上具有此功能。)

其次,ForEach方法的方法存在局限性。如果您使用yield return实现IEnumerable,则无法在lambda内部进行yield return。因此,通过这种方法循环访问集合中的项以返回内容是不可能的。您将不得不使用foreach关键字,并通过手动在循环内部制作当前循环值的副本来解决闭包问题。

更多信息请参见此处


7
你提到的关于foreach的问题已经在C# 5中得到了“修复”。https://dev59.com/Vmox5IYBdhLWcg3w6oZf - StriplingWarrior

6

你可以给匿名委托命名 :-)

第二个可以写成:

someList.ForEach(s => s.ToUpper())

我更喜欢这种方式,因为它可以节省很多打字。
正如Joachim所说,对于第二种形式来说,并行处理更容易应用。

5
List.ForEach()被认为更具功能性。 List.ForEach() 告诉你想要做什么,而 foreach(item in list) 也确切地说明了你希望如何完成。这使得 List.ForEach 可以在未来自由更改实现的部分。例如,.Net 的一个假设的未来版本可能总是并行运行 List.ForEach,因为此时每个人都有一些通常空闲的 CPU 核心。
另一方面,foreach (item in list) 让您对循环有更多控制。例如,您知道项目将按某种顺序迭代,并且如果项目满足某些条件,您可以轻松中断。
关于这个问题的一些最新评论,请参见:

https://dev59.com/ZXRB5IYBdhLWcg3wvpmc#529197


3

整个ForEach作用域(委托函数)被视为单行代码(调用函数),您无法设置断点或进入代码。如果发生未处理的异常,整个代码块将被标记。


2

ForEach函数是泛型类List的成员。

我创建了以下扩展来复制内部代码:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

因此,最终我们使用普通的foreach(或者如果您想要的话,可以使用for循环)。

另一方面,使用委托函数只是定义函数的另一种方式。以下是代码示例:

delegate(string s) {
    <process the string>
}

等价于:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

或者使用lambda表达式:

(s) => <process the string>

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