foreach vs somlist . foreach (){}

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

第一类型:

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

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

314869 次浏览
我猜someList.ForEach()调用可以很容易地并行,而正常的foreach不那么容易并行运行。 你可以很容易地在不同的核心上运行几个不同的委托,这对于普通的foreach.
来说并不容易做到 只是我的2美分

你可以命名匿名委托:-)

你可以把第二个写成

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

我更喜欢这个,而且节省了很多打字。

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

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

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

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

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

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

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

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

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

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


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

https://stackoverflow.com/a/529197/3043 < a href = " https://stackoverflow.com/a/529197/3043 " > < / >

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

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

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

为了好玩,我将List弹出到reflector中,结果是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;
}

列表中。ForEach比MoveNext更精简——处理更少——将更有可能JIT成高效的东西。

此外,foreach()将分配一个新的Enumerator。GC是你的朋友,但如果你重复做同样的foreach,这将产生更多的一次性对象,而不是重用同一个委托————这确实是一个边缘情况。在典型用法中,您将看到很少或没有区别。

我们这里有一些代码(在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)构造允许你在需要退出迭代或循环时使用breakcontinue。但是不能在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()更具体的LINQ扩展方法。看看Where()Select()Any()All()Max()或许多其他LINQ方法之一是否还没有从循环中做你想要的事情。

我知道有两件事让他们不一样。去我!

首先,有一个典型的错误,即为列表中的每个项目创建一个委托。如果你使用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();

列表中。每个方法都没有这个问题。迭代的当前项按值作为参数传递给外部lambda,然后内部lambda在自己的闭包中正确地捕获该参数。问题解决了。

(遗憾的是,我认为ForEach是List的成员,而不是扩展方法,尽管很容易自己定义它,因此您可以在任何可枚举类型上使用此功能。)

其次,ForEach方法有一定的局限性。如果你通过使用yield return来实现IEnumerable,你不能在lambda中做yield return。因此,通过循环遍历集合中的项以获得返回值是不可能的。您必须使用foreach关键字,并通过手动在循环中复制当前循环值来解决闭包问题。

更多的这里

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

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

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

等价于:

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

或者使用labda表达式:

(s) => <process the string>

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

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

这两种集合枚举方法的最大区别是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>的集合都会做同样的事情。这似乎明显违反了利斯科夫替换原则:

父类的对象可以被父类的对象替换

另一个问题是枚举器必须实现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;指数& lt;这一点。_size,,... ; + +指数) 操作(this._items(指数));< /代码> < / p >

此代码不分配任何枚举数(没有分配给Dispose),并且在迭代时不分配暂停

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

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

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

总有第三个选择……

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

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

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