foreach与someList.ForEach(){}


167

显然,有很多方法可以迭代集合。好奇是否存在任何差异,或者为什么要使用一种方法来替代另一种方法。

第一类:

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

我想想起来,您将可以指定一个可重用的委托,而不是上面使用的匿名委托...


Answers:


134

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

因为.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循环内的列表中删除项目可能会有副作用。在此问题的注释中描述了最常见的一个。

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


35
即使这样,您也应该改用someList.RemoveAll(x => x.RemoveMe)
Mark Cidade,

2
有了Linq,所有事情都可以做得更好。我只是展示了一个在foreach中修改集合的示例...

2
RemoveAll()是List <T>上的方法。
Mark Cidade

3
您可能已经意识到了这一点,但是人们应该提防以这种方式删除项目。如果删除项目N,则迭代将跳过项目(N + 1),并且您不会在委托中看到它,也没有机会删除它,就像您在自己的for循环中这样做一样。
El Zorko 2011年

9
如果使用for循环向后迭代列表,则可以删除项目而不会出现任何索引移位问题。
S.TarıkÇetin16年

78

我们这里有一些代码(在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#:50种改善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循环将控制权交给编译器。

我的感觉是,在一个新项目中,当您可以使用C#3 “运算符更优雅或更紧凑地执行某些操作时,可以将foreach(item in list)大多数循环用于坚持通用用法和可读性,并且list.Foreach()仅用于短块=>。在这种情况下,可能已经存在比LINQ更具体的LINQ扩展方法ForEach()。看看Where()Select()Any()All()Max()或许多其他LINQ方法中的一种尚不你从循环想要的东西。


只是出于好奇......看看微软实现... referencesource.microsoft.com/#mscorlib/system/collections/...
亚历山大

17

为了娱乐,我将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;
}

与MoveNext相比,List.ForEach的内容要精简得多-处理量少得多-很有可能将JIT转换为有效的内容。

另外,无论如何,foreach()都会分配一个新的Enumerator。GC是您的朋友,但是如果您重复执行相同的foreach动作,则将产生更多的可丢弃对象,而不是重用同一委托-BUT-这确实是一种附带情况。在典型用法中,您几乎看不到差异。


1
您不能保证在编译器版本之间,由foreach生成的代码是相同的。将来的版本可能会改进生成的代码。
安东尼

1
从.NET Core 3.1开始,ForEach仍然更快。
杰克莫特

13

我知道两件事使它们与众不同。去吧

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

这里更多


5
您提到的问题foreach已在C#5中“修复” 。stackoverflow.com
questions/8898925/…

13

我猜想someList.ForEach()调用可以很容易地并行化,而正常foreach情况下并行运行则不那么容易。您可以轻松地在不同的内核上运行多个不同的委托,而使用normal则不那么容易foreach
就是我的2美分


2
我认为他的意思是运行时引擎可以自动并行化它。否则,可以使用每个动作委托中的池中的线程来手动并行化foreach和.ForEach
Isak Savo

@Isak,但这将是一个错误的假设。如果匿名方法将本地或成员提升,则运行时将不容易8)可以并行化-Rune
FS

6

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

两种收集枚举方法之间的最大区别是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]);
}

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

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


5

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

您可以将第二个写为:

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

我更喜欢这种方式,并且省去了很多打字工作。

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


2

在后台,匿名委托被转换为实际方法,因此如果编译器不选择内联该函数,则第二选择可能会产生一些开销。此外,匿名委托示例的主体引用的任何局部变量的性质都会发生变化,这是因为编译器将其隐藏为已被编译为新方法的事实所致。有关C#如何实现此魔术的更多信息,请参见:

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


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>

2

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


2

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

List.ForEach()说你想做什么。 foreach(item in list)还准确地说出您要如何完成。这使得List.ForEach随意改变的实施如何在未来的一部分。例如,假设的.Net未来版本可能始终运行List.ForEach并行,但前提是每个人此时都有许多通常处于闲置状态的cpu内核。

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


有关此问题的更多最新评论,请单击此处:

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


1

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

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

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


1

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


0

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

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

您可以使用foreach中的项目

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.