196

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

第一种:

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

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

4

13 回答 13

150

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

因为 .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);

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

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

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

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

于 2008-10-22T14:50:46.683 回答
85

我们这里有一些代码(在 VS2005 和 C#2.0 中),以前的工程师不遗余力地使用这些代码,list.ForEach( delegate(item) { foo;});而不是foreach(item in list) {foo; };他们编写的所有代码。例如,用于从 dataReader 读取行的代码块。

我仍然不知道他们为什么这样做。

的缺点list.ForEach()是:

  • 它在 C# 2.0 中更加冗长。但是,从 C# 3 开始,您可以使用 " =>" 语法来制作一些非常简洁的表达式。

  • 它不太熟悉。必须维护此代码的人会想知道您为什么这样做。我花了一段时间才确定没有任何理由,除了可能让作者看起来很聪明(其余代码的质量破坏了这一点)。它的可读性也较差,})在委托代码块的末尾带有“”。

  • 另请参阅 Bill Wagner 的书“Effective C#: 50 Specific Ways to Improvement Your C#”,其中他谈到了为什么 foreach 比其他循环(如 for 或 while 循环)更受欢迎 - 重点是让编译器决定构建的最佳方式循环。如果编译器的未来版本设法使用更快的技术,那么您将通过使用 foreach 和重新构建来免费获得它,而不是更改您的代码。

  • 一个foreach(item in list)构造允许您使用break或者continue如果您需要退出迭代或循环。但是您不能更改 foreach 循环内的列表。

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

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

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

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

于 2008-10-22T15:28:58.540 回答
18

为了好玩,我将 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 使用的 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() 无论如何都会分配一个新的 Enumerator。GC 是您的朋友,但是如果您重复执行相同的 foreach,这将产生更多的一次性对象,而不是重用相同的委托 -但是- 这确实是一个边缘案例。在典型用法中,您会发现很少或没有区别。

于 2008-10-22T14:53:48.080 回答
15

正如他们所说,魔鬼在细节中......

两种集合枚举方法的最大区别是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. 集合的副本是浅拷贝。

这当然不是线程安全的。正如上面所指出的,在迭代时更改集合只是不好的魔力。

但是,在迭代期间通过我们之外的方式在迭代期间对集合进行处理而导致集合变得无效的问题呢?最佳实践建议在操作和迭代期间对集合进行版本控制,并检查版本以检测底层集合何时更改。

这就是事情变得非常模糊的地方。根据微软文档:

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

嗯,那是什么意思?举例来说,仅仅因为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]);
}

它不性感,但它有牙齿(向汤姆克鲁斯和电影The Firm道歉)

这是你的选择,但现在你知道了,它可以成为一个知情的人。

于 2019-12-11T01:03:16.907 回答
14

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

于 2008-10-22T14:22:48.750 回答
14

我知道有两件晦涩难懂的东西让它们与众不同。走我!

首先,存在为列表中的每个项目创建委托的经典错误。如果您使用 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 关键字并通过在循环内手动制作当前循环值的副本来解决闭包问题。

更多在这里

于 2008-10-22T15:41:40.213 回答
6

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

你可以把第二个写成:

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

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

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

于 2008-10-22T14:24:52.630 回答
5

List.ForEach() 被认为更实用。

List.ForEach()说你想做什么。 foreach(item in list)还准确地说明了您希望它如何完成。这使得将来List.ForEach可以自由更改how部分的实现。例如,假设List.ForEach每个人都有许多通常处于空闲状态的 cpu 内核,假设的未来版本的 .Net 可能总是并行运行。

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


此处提供了有关此问题的一些最新评论:

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

于 2008-10-22T14:41:38.837 回答
2

在幕后,匿名委托变成了一个实际的方法,所以如果编译器没有选择内联函数,你可能会在第二个选择中产生一些开销。此外,匿名委托示例的主体引用的任何局部变量本质上都会发生变化,因为编译器技巧会隐藏它被编译为新方法的事实。有关 C# 如何做到这一点的更多信息:

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

于 2008-10-22T14:26:44.857 回答
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 (或者如果你想要一个循环)。

另一方面,使用委托函数只是定义函数的另一种方式,这段代码:

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

相当于:

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

或使用 labda 表达式:

(s) => <process the string>
于 2014-04-25T10:50:04.600 回答
2

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

于 2016-03-31T06:49:57.220 回答
1

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

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

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

于 2008-10-22T14:25:43.580 回答
1

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

于 2008-10-22T14:35:55.543 回答