foreach 与 someList.ForEach(){}

foreach vs someList.ForEach(){}

提问人:Bryce Fischer 提问时间:10/22/2008 最后编辑:Peter BruinsBryce Fischer 更新时间:5/28/2020 访问量:342834

问:

显然,有很多方法可以循环访问集合。好奇是否有任何差异,或者为什么你会使用一种方式而不是另一种方式。

第一种类型:

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>
});

我想在我的脑海中,而不是我上面使用的匿名委托,你会有一个可重用的委托,你可以指定......

C# .NET 泛型 循环 枚举

评论

6赞 Erik Philips 11/21/2017
我建议阅读 Eric Lipperts 的博客“foreach”与“ForEach”
6赞 jasie 7/7/2021
@ErikPhilips 该链接已过时。不过,这篇文章仍然可以在这里找到:ericlippert.com/2009/05/18/foreach-vs-foreach

答:

16赞 Joachim Kerschbaumer 10/22/2008 #1

我想调用可以很容易地并行化,而正常调用并行运行并不容易。 您可以轻松地在不同的内核上运行多个不同的委托,这对于普通的 .
只有我的 2 美分
someList.ForEach()foreachforeach

评论

2赞 Isak Savo 10/24/2008
我认为他的意思是运行时引擎可以自动并行化它。否则,foreach 和 .ForEach 可以使用池中的线程手动并行化每个操作委托
1赞 Rune FS 1/14/2012
@Isak但这是一个不正确的假设。如果匿名方法提升了本地或成员,则运行时将不 8easy) 能够并行
6赞 Craig.Nicol 10/22/2008 #2

您可以命名匿名代表:-)

你可以把第二个写成:

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

我更喜欢,并节省了很多打字时间。

正如 Joachim 所说,并行性更容易应用于第二种形式。

1赞 EFrank 10/22/2008 #3

您展示的第二种方法使用扩展方法对列表中的每个元素执行委托方法。

这样,您就有了另一个委托 (=method) 调用。

此外,还可以使用 for 循环迭代列表。

2赞 jezell 10/22/2008 #4

在幕后,匿名委托会变成一个实际的方法,因此,如果编译器没有选择内联函数,则第二个选择可能会产生一些开销。此外,匿名委托示例的正文引用的任何局部变量的性质都会发生变化,因为编译器会隐藏它被编译为新方法的事实。有关 C# 如何实现这种魔力的更多信息,请参阅此处:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx

1赞 Chris Kimpton 10/22/2008 #5

需要警惕的一件事是如何退出 Generic .ForEach 方法 - 请参阅此讨论。虽然链接似乎说这种方式是最快的。不知道为什么 - 你会认为它们一旦编译就会是等价的......

5赞 Joel Coehoorn 10/22/2008 #6

List.ForEach() 被认为功能更强大。

List.ForEach()说出你想做的事情。 还准确地说明了您希望如何完成。这样就可以自由地更改将来如何部分的实现。例如,假设 .Net 的未来版本可能始终并行运行,前提是此时每个人都有许多通常处于空闲状态的 CPU 内核。foreach(item in list)List.ForEachList.ForEach

另一方面,让您对循环有更多的控制权。例如,您知道这些项目将按某种顺序进行迭代,如果某个项目满足某些条件,则很容易在中间中断。foreach (item in list)


关于这个问题的一些最新评论可在此处获得:

https://stackoverflow.com/a/529197/3043

162赞 user1228 10/22/2008 #7

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

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

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

而使用枚举器,因此这是无效的:foreach

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

TL的;dr:不要将此代码复制粘贴到您的应用程序中!

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

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

通常,如果要从列表中删除多个项目,则需要将要删除哪些项目的决定与实际删除分开。它不会使您的代码保持紧凑,但它可以保证您不会遗漏任何项目。

评论

41赞 Mark Cidade 10/22/2008
即便如此,您也应该使用 someList.RemoveAll(x => x.RemoveMe)
4赞 10/23/2008
有了 Linq,所有事情都可以做得更好。我只是展示了一个在 foreach 中修改集合的示例......
2赞 Mark Cidade 10/24/2008
RemoveAll() 是 List<T> 上的一个方法。
3赞 El Zorko 4/25/2011
您可能已经意识到这一点,但人们应该注意以这种方式删除物品;如果删除项目 N,则迭代将跳过项目 (N+1),您将不会在委托中看到它,也不会有机会删除它,就像您在自己的 for 循环中执行此操作一样。
13赞 starikcetin 8/25/2016
如果使用 for 循环向后迭代列表,则可以删除项,而不会出现任何索引移位问题。
19赞 plinth 10/22/2008 #8

为了好玩,我把 List 弹出到反射器中,这就是生成的 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 使用的枚举器中的 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赞 Anthony 3/23/2009
您不能保证 foreach 生成的代码在编译器版本之间是相同的。生成的代码可能会在将来的版本中改进。
3赞 jackmott 2/1/2020
从 .NET Core 3.1 开始,ForEach 的速度仍然更快。
88赞 10 revsAnthony #9

我们这里有一些代码(在 VS2005 和 C#2.0 中),以前的工程师不遗余力地使用而不是他们编写的所有代码。例如,用于从 dataReader 读取行的代码块。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 和重新构建来免费获得它,而不是更改您的代码。

  • 构造允许您使用 OR 是否需要退出迭代或循环。但是,您不能更改 foreach 循环中的列表。foreach(item in list)breakcontinue

我很惊讶地发现它稍微快了一点。但这可能不是自始至终使用它的正当理由,那将是过早的优化。如果您的应用程序使用数据库或 Web 服务,而不是循环控制,则几乎总是时间流向何方。你是否也对它进行了循环基准测试?由于在内部使用它,可能会更快,而没有包装器的循环会更快。list.ForEachforlist.ForEachfor

我不同意该版本在任何重要方面都“功能更强大”。它确实将一个函数传递给一个函数,但在结果或项目组织方面没有太大区别。list.ForEach(delegate)

我不认为这“确切地说了你想要它如何完成”——循环做到了这一点,循环将控制权的选择权留给了编译器。foreach(item in list)for(int 1 = 0; i < count; i++)foreach

我的感觉是,在一个新项目中,为了遵守常用用法和可读性,大多数循环都应使用,并且仅用于短块,这时您可以使用 C# 3 “” 运算符更优雅或更紧凑地执行某些操作。在这种情况下,可能已经存在比 更具体的 LINQ 扩展方法。查看 、 、 、 或许多其他 LINQ 方法之一是否尚未从循环中执行所需的操作。foreach(item in list)list.Foreach()=>ForEach()Where()Select()Any()All()Max()

评论

0赞 Alexandre 1/4/2015
只是为了好奇......看看 Microsoft 的实现...referencesource.microsoft.com/#mscorlib/system/collections/......
15赞 Daniel Earwicker 10/22/2008 #10

我知道两件晦涩难懂的事情,使它们与众不同。走吧!

首先,有一个经典的错误,即为列表中的每个项目创建一个委托。如果使用 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赞 StriplingWarrior 11/26/2014
您提到的问题在 C# 5 中已“修复”。stackoverflow.com/questions/8898925/......foreach
2赞 Pablo Caballero 4/25/2014 #11

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(或者如果你愿意的话,也可以使用循环)。

另一方面,使用委托函数只是定义函数的另一种方法,如下代码:

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

相当于:

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

或使用 Labda 表达式:

(s) => <process the string>
3赞 Peter Shen 3/31/2016 #12

整个 ForEach 作用域(委托函数)被视为一行代码(调用函数),不能设置断点或单步执行代码。如果发生未经处理的异常,则标记整个块。

23赞 Stacy Dudovitz 12/11/2019 #13

正如他们所说,细节决定成败......

两种集合枚举方法之间的最大区别是携带状态,而不携带状态。foreachForEach(x => { })

但是让我们更深入地挖掘一下,因为您应该注意一些会影响您的决定的事情,并且在为这两种情况编码时都应该注意一些注意事项。

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

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>

超类的对象应替换为其 在不破坏应用程序的情况下进行子类。

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

最后,我们有一个终生的问题......如果迭代器有效,但基础集合消失了,会发生什么情况?我们现在简要介绍一下当时的...当您将集合的生存期与其迭代器分开时,您是在自找麻烦。

现在让我们检查一下: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

是的,我说的是迭代器......有时拥有状态是有利的。假设您想要维护类似于数据库游标的东西...也许多种风格是要走的路。我个人不喜欢这种设计风格,因为有太多的终生问题,而且你依赖于你所依赖的收藏品作者的善意(除非你真的从头开始自己写所有东西)。foreachIterator<T>

总有第三种选择......

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

它并不性感,但它有牙齿(向汤姆克鲁斯和电影《公司》道歉)

这是你的选择,但现在你知道了,它可以是一个明智的选择。