foreach vs someList.ForEach(){}

时间:2008-10-22 14:19:07

标签: c# .net generics loops enumeration

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

第一种类型:

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

我认为,除了我上面使用的匿名代表之外,我还有一个你可以指定的可重用代理......

14 个答案:

答案 0 :(得分:115)

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

因为.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;博士:请勿将此代码复制到您的应用中!

这些示例不是最佳做法,只是为了展示ForEach()foreach之间的差异。

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

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

答案 1 :(得分:71)

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

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

list.ForEach()的缺点是:

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

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

  • 另请参阅Bill Wagner的书“有效的C#:提高C#的50种具体方法”,其中他谈到为什么foreach比其他循环更喜欢for或while循环 - 主要的一点是你让编译器决定构建循环的最佳方法。如果未来版本的编译器设法使用更快的技术,那么您将通过使用foreach和重建来免费获得,而不是更改您的代码。

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

我很惊讶地看到list.ForEach稍快一些。但这可能不是一直使用它的正当理由,那将是不成熟的优化。如果您的应用程序使用的数据库或Web服务,而不是循环控制,几乎总是会在时间的推移。你有没有对for循环进行基准测试?由于在内部使用list.ForEachfor可能更快,而没有包装器的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()更具体的LINQ扩展方法。查看Select()Any()All()Max(),{{1}}或许多其他LINQ方法中的一个尚未从循环中执行您想要的操作。

答案 2 :(得分:16)

为了好玩,我将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更加严格 - 处理更少 - 更有可能J​​IT变成高效的东西..

此外,无论如何,foreach()都会分配一个新的枚举器。 GC是你的朋友,但如果你反复做同样的事情,这会产生更多的一次性物品,而不是重复使用相同的代表 - 但是 - 这实际上是一个边缘情况。在典型的使用中,您将看到很少或没有差异。

答案 3 :(得分:11)

我知道两件让他们与众不同的晦涩难懂的事物。去吧!

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

More here

答案 4 :(得分:10)

我想someList.ForEach()调用可以轻松并行化,而普通foreach并不容易并行。 您可以轻松地在不同的核心上运行多个不同的代理,这对于普通的foreach来说并不容易 只差我2美分

答案 5 :(得分:3)

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

你可以把第二个写成:

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

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

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

答案 6 :(得分:2)

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

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

答案 7 :(得分:2)

List.ForEach()被认为更具功能性。

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

另一方面,foreach (item in list)使您可以更多地控制循环。例如,您知道项目将以某种顺序迭代,如果某个项目符合某些条件,您可以轻松地在中间进行中断。

答案 8 :(得分:2)

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

答案 9 :(得分:2)

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

这两种集合枚举方法之间的最大区别是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>的集合都将执行相同的操作。这似乎明显违反了《里斯科夫换人原则》:

  

超类的对象应可替换为其超类的对象   子类而不破坏应用程序。

另一个问题是,枚举器必须实现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]);
}

它不是性感的,但是有牙齿(对 Tom Cruise 和电影 The Firm 的道歉)

这是您的选择,但现在您知道了,它可以成为有见识的人。

答案 10 :(得分:1)

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

这样,你有另一个委托(=方法)调用。

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

答案 11 :(得分:1)

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

答案 12 :(得分:1)

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>

答案 13 :(得分:0)

有一种方法,我已经在我的应用程序中完成了:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

您可以使用foreach中的

作为项目