显然有许多方法可以迭代集合。很好奇是否有什么不同,或者为什么你用一种方式而不是另一种。

第一类型:

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

我想,除了我上面使用的匿名委托,你还可以指定一个可重用的委托。


当前回答

常言道,细节决定成败……

两种集合枚举方法的最大区别是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;
  }
}

有两点是显而易见的:

返回一个有状态对象,并对底层集合了如指掌。 集合的副本是浅副本。

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

但是在迭代过程中,由于我们之外的人在迭代过程中破坏集合而导致集合失效的问题怎么办?最佳实践建议在操作和迭代期间对集合进行版本控制,并检查版本以检测底层集合何时发生变化。

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

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

那是什么意思呢?举例来说,仅仅因为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;索引< this。_size &&…; + +指数) 操作(this._items(指数));

这段代码没有分配任何枚举数(没有要Dispose的枚举数),并且在迭代时没有暂停。

注意,这也执行底层集合的浅拷贝,但是集合现在是一个及时快照。如果作者没有正确地实现集合更改或“过时”的检查,快照仍然有效。

这并不能以任何方式保护你免受终身问题的困扰……如果底层的集合消失了,你现在就有了一个浅拷贝,它指向…但至少你不需要处理孤立迭代器上的Dispose问题…

是的,我说的是迭代器……有时拥有国家是有利的。假设您想要维护类似于数据库游标的东西……也许多重foreach风格的Iterator<T>'s是可行的。我个人不喜欢这种设计风格,因为有太多的寿命问题,你依赖于你所依赖的集合的作者的好意(除非你真的从头开始写所有东西)。

总有第三个选择……

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

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

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

其他回答

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

List.ForEach()表示你想做什么。Foreach(列表中的项目)也确切地说明了你想要如何完成它。这就剩下List了。ForEach可以自由更改未来如何部分的实现。例如,一个假设的。net未来版本可能总是运行List。在并行的情况下,假设此时每个人都有一些cpu核心通常处于空闲状态。

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


关于这个问题的一些最新评论可在此查阅:

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

需要注意的一件事是如何退出Generic . foreach方法——请参阅本讨论。虽然链接上似乎写着这条路最快。不知道为什么-你会认为一旦编译它们就会等价…

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

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

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

因为.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循环中从列表中删除项可能会产生副作用。最常见的是在这个问题的评论中描述的。

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

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

我还是不知道他们为什么要这么做。

list.ForEach()的缺点是:

It is more verbose in C# 2.0. However, in C# 3 onwards, you can use the "=>" syntax to make some nicely terse expressions. It is less familiar. People who have to maintain this code will wonder why you did it that way. It took me awhile to decide that there wasn't any reason, except maybe to make the writer seem clever (the quality of the rest of the code undermined that). It was also less readable, with the "})" at the end of the delegate code block. See also Bill Wagner's book "Effective C#: 50 Specific Ways to Improve Your C#" where he talks about why foreach is preferred to other loops like for or while loops - the main point is that you are letting the compiler decide the best way to construct the loop. If a future version of the compiler manages to use a faster technique, then you will get this for free by using foreach and rebuilding, rather than changing your code. a foreach(item in list) construct allows you to use break or continue if you need to exit the iteration or the loop. But you cannot alter the list inside a foreach loop.

看到这个名单我很惊讶。ForEach稍微快一点。但这可能不是贯穿始终的有效理由,那将是不成熟的优化。如果你的应用程序使用的是数据库或web服务,而不是循环控制,那么时间几乎总是会花在那里。你也用for循环测试过它吗?列表中。ForEach可以更快,因为在内部使用它,没有包装器的for循环甚至更快。

我不同意list.ForEach(委托)版本在任何重要方面都“更实用”。它确实将一个函数传递给另一个函数,但在结果或程序组织方面没有太大的区别。

我不认为每一项都是这样。“确切地说你想怎么做”- a for(int 1 = 0;I < count;i++)循环可以做到这一点,foreach循环将控制的选择留给编译器。

My feeling is, on a new project, to use foreach(item in list) for most loops in order to adhere to the common usage and for readability, and use list.Foreach() only for short blocks, when you can do something more elegantly or compactly with the C# 3 "=>" operator. In cases like that, there may already be a LINQ extension method that is more specific than ForEach(). See if Where(), Select(), Any(), All(), Max() or one of the many other LINQ methods doesn't already do what you want from the loop.