Why should I use foreach instead of for (int i=0; i<length; i++) in loops?
C语言和Java中循环的酷方法似乎是使用Frach而不是C样式来循环。
我为什么喜欢这种样式而不是C样式呢?
我对这两件事特别感兴趣,但请尽可能多地说明你的观点。
- 我希望对列表中的每个项目执行操作。
- 我正在列表中搜索项目,希望在找到该项目时退出。
想象一下,你是一家餐厅的主厨,你们都在为自助餐准备一个巨大的煎蛋。你把一盒一打的鸡蛋递给厨房里的两个工作人员,然后告诉他们要裂开,字面意思是。
第一个建立了一个碗,打开板条箱,轮流抓住每个鸡蛋-从左到右横跨最上面一行,然后最下面一行-打破它对碗的一边,然后把它倒进碗里。最后,他没有鸡蛋了。干得好的工作。
第二个放一个碗,打开板条箱,然后冲出去拿一张纸和一支笔。他把数字0到11写在鸡蛋盒的隔仓旁边,把数字0写在纸上。他看了看纸上的数字,找到一个标有0的盒子,取出鸡蛋,把它放进碗里。他又看了看纸上的0,想"0+1=1",划掉0,在纸上写1。他从1号舱抓起鸡蛋,把它弄碎。等等,直到12号写在纸上,他才知道(不看!)没有鸡蛋了。干得好的工作。
你会认为第二个家伙脑子有点乱,对吧?
用高级语言工作的目的是避免用计算机术语描述事物,并且能够用自己的术语描述事物。语言层次越高,这就越真实。在循环中增加计数器会分散您真正想要做的事情:处理每个元素。
除此之外,通过增加计数器和索引,"索引"意味着从头开始重新计数,无法有效地处理链接列表类型的结构。在C语言中,我们可以通过使用循环"counter"的指针并取消对它的引用来处理我们自己创建的链接列表。我们可以使用"迭代器"在现代C++(在C语言和Java语言中)做到这一点,但这仍然存在间接性问题。
最后,有些语言的水平很高,以至于实际编写一个循环来"对列表中的每个项目执行操作"或"在列表中搜索项目"的想法令人震惊(就像厨师长不必告诉第一个厨房员工如何确保鸡蛋裂了)。提供了设置该循环结构的函数,您可以通过更高阶函数(在搜索案例中可能是比较值)告诉它们在循环中要做什么。(事实上,您可以在C++中做这些事情,尽管接口有些笨拙。)
我能想到的两个主要原因是:
1)它从底层容器类型抽象出来。例如,这意味着,当您更改容器时,不必更改在容器中所有项目上循环的代码——您指定的目标是"为容器中的每个项目执行此操作",而不是方法。
2)它消除了一次错误关闭的可能性。
在对列表中的每个项目执行操作时,可以直观地说:
1 2 3 4 | for(Item item: lst) { op(item); } |
。
它完美地向读者表达了意图,而不是用迭代器手工做一些事情。同上,用于搜索项目。
foreach 更简单易读。对于链表这样的结构,它可能更有效
并非所有集合都支持随机访问;迭代
HashSet 或Dictionary 的唯一方法是.KeysCollection foreach 。foreach 允许您迭代方法返回的集合,而不需要额外的临时变量:1foreach(var thingy in SomeMethodCall(arguments)) { ... }
对我来说,一个好处是不太容易犯错误,比如
1 2 3 4 5 | for(int i = 0; i < maxi; i++) { for(int j = 0; j < maxj; i++) { ... } } |
号
更新:这是错误发生的一种方式。我算一笔钱
1 2 3 4 | int sum = 0; for(int i = 0; i < maxi; i++) { sum += a[i]; } |
然后决定把它加起来。所以我用另一个圈把它包起来。
1 2 3 4 5 6 7 8 | int total = 0; for(int i = 0; i < maxi; i++) { int sum = 0; for(int i = 0; i < maxi; i++) { sum += a[i]; } total += sum; } |
。
当然,编译失败,所以我们手工编辑
1 2 3 4 5 6 7 8 | int total = 0; for(int i = 0; i < maxi; i++) { int sum = 0; for(int j = 0; j < maxj; i++) { sum += a[i]; } total += sum; } |
现在代码中至少有两个错误(如果我们混淆了maxi和maxj,则会有更多错误),这些错误只能由运行时错误检测到。如果你不写测试…这是一段罕见的代码——它会咬到别人——很严重。
这就是为什么将内部循环提取到方法中是一个好主意:
1 2 3 4 5 6 7 8 9 10 11 12 | int total = 0; for(int i = 0; i < maxi; i++) { total += totalTime(maxj); } private int totalTime(int maxi) { int sum = 0; for(int i = 0; i < maxi; i++) { sum += a[i]; } return sum; } |
。
而且可读性更强。
但是,与
如我们所见,在大多数情况下使用
也就是说,如果您出于其他目的需要
[1]"在所有场景中"实际上是指"集合易于迭代的所有场景",这实际上是"大多数场景"(参见下面的注释)。然而,我真的认为涉及到一个不友好的迭代集合的迭代场景必须被设计出来。
如果您将C作为一种语言作为目标,那么您可能也应该考虑使用LINQ,因为这是执行循环的另一种逻辑方法。
通过对列表中的每个项目执行操作,您的意思是在列表中对其进行适当的修改,还是简单地对项目进行一些操作(例如打印、累积、修改等)?我怀疑是后者,因为C中的
下面是两个简单的构造,首先使用
1 2 3 4 5 6 7 | List<string> list = ...; List<string> uppercase = new List<string> (); for (int i = 0; i < list.Count; i++) { string name = list[i]; uppercase.Add (name.ToUpper ()); } |
(请注意,在.NET中使用结束条件
这是
1 2 3 4 5 6 | List<string> list = ...; List<string> uppercase = new List<string> (); foreach (name in list) { uppercase.Add (name.ToUpper ()); } |
号
注:基本上,
这里有几个我能想到的等价的解决方案,用c linq表示(它引入了lambda表达式的概念,基本上是一个内联函数,采用
1 2 3 | List<string> list = ...; List<string> uppercase = new List<string> (); uppercase.AddRange (list.Select (x => x.ToUpper ())); |
或者使用由其构造函数填充的
1 2 | List<string> list = ...; List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ())); |
。
或者使用
1 2 | List<string> list = ...; List<string> uppercase = list.Select (x => x.ToUpper ()).ToList (); |
或与类型推断相同:
1 2 | List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ()).ToList (); |
。
或者,如果您不介意得到一个
1 2 | List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ()); |
。
或者另一个使用c sql的关键字,比如
1 2 3 | List<string> list = ...; var uppercase = from name in list select name => name.ToUpper (); |
Linq非常有表现力,而且经常,我觉得代码比简单的循环更具可读性。
您的第二个问题,搜索列表中的一个项目,并希望在找到该项目时退出,也可以非常方便地使用LINQ实现。下面是一个
1 2 3 4 5 6 7 8 9 10 | List<string> list = ...; string result = null; foreach (name in list) { if (name.Contains ("Pierre")) { result = name; break; } } |
。
下面是简单的LINQ等价物:
1 2 | List<string> list = ...; string result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault (); |
或者使用查询语法:
1 2 3 4 5 6 | List<string> list = ...; var results = from name in list where name.Contains ("Pierre") select name; string result = results.FirstOrDefault (); |
。
我希望这能给
正如斯图亚特·戈洛德茨所回答的,这是一种抽象。
如果您只使用
1 2 3 4 | String[] lines = getLines(); for( int i = 0 ; i < 10 ; ++i ) { System.out.println("line" + i + lines[i] ) ; } |
那么就不需要知道
1 2 3 4 5 | Line[] pages = getPages(); for( int i = 0 ; i < 10 ; ++i ) { for( int j = 0 ; j < 10 ; ++i ) System.out.println("page" + i +"line" + j + page[i].getLines()[j]; } |
号
正如Andrew Koenig所说,"抽象是选择性的无知";如果您不需要知道如何迭代某些集合的细节,那么可以找到忽略这些细节的方法,然后编写更健壮的代码。
使用foreach的原因:
- 它可以防止错误进入(例如,在for循环中忘记了
i++ ),从而导致循环出现故障。有很多种方法可以让循环更糟糕,但是没有很多方法可以让每个循环更糟糕。 - 它看起来更干净/不那么神秘。
- 在某些情况下(例如,如果您有一个不能像
IList can那样被索引的IEnumerable ),for 循环甚至不可能实现。
使用
- 在遍历平面列表(数组)时,这些类型的循环具有轻微的性能优势,因为不存在使用枚举器创建的额外间接级别。(然而,这种性能增益是最小的。)
- 要枚举的对象不实现
IEnumerable --foreach ,只对可枚举对象进行操作。 - 其他特殊情况;例如,如果要从一个数组复制到另一个数组,
foreach 将不会为您提供可用于寻址目标数组槽的索引变量。在这种情况下,for 是唯一有意义的。
在使用任何一个循环时,您在问题中列出的两个案例实际上都是相同的——首先,您只需迭代到列表的末尾,然后在第二个循环中,当您找到要查找的项目后,您将执行ecx1(14)。
为了进一步解释
1 2 3 4 5 | IEnumerable<Something> bar = ...; foreach (var foo in bar) { // do stuff } |
。
句法糖用于:
1 2 3 4 5 6 7 8 9 10 11 12 | IEnumerable<Something> bar = ...; IEnumerator<Something> e = bar.GetEnumerator(); try { Something foo; while (e.MoveNext()) { foo = e.Current; // do stuff } } finally { ((IDisposable)e).Dispose(); } |
号
如果要在实现IEnumerable的集合上迭代,则更自然地使用foreach,因为迭代中的下一个成员是在完成到达结尾的测试的同时指定的。例如。,
1 | foreach (string day in week) {/* Do something with the day ... */} |
比
1 | for (int i = 0; i < week.Length; i++) { day = week[i]; /* Use day ... */ } |
。
也可以在类自己的IEnumerable实现中使用for循环。只需让getEnumerator()实现在循环体中使用c yield关键字:
1 | yield return my_array[i]; |
在第一种情况下,您应该使用经典的(C样式)for循环。但在第二种情况下,您应该使用
在第一种情况下,也可以使用
我能想到几个原因
请注意,如果您在列表中为每个项目搜索一个项目,那么很可能是做错事了。考虑使用
假设程序员希望使用
1 2 3 4 | for ( Iterator<T> elements = input.iterator(); elements.hasNext(); ) { // Inside here, nothing stops programmer from calling `element.next();` // more then once. } |
至少在Java中不使用Frach的一个原因是它将创建一个迭代器对象,该对象最终将被垃圾收集。因此,如果您试图编写避免垃圾收集的代码,最好避免foreach。但是,我相信对于纯数组来说是可以的,因为它不创建迭代器。
如果您可以使用
(您的两种方案在使用
看起来大部分的东西都被覆盖了…以下是关于您的具体问题,我没有看到提到的一些额外注释。这些都是硬规则,与样式首选项不同:
I wish to perform an operation on each item in a list
号
在
值得注意的是,"酷"的方法现在是使用LINQ;如果您感兴趣,可以搜索大量资源。
对于实现大量收集,foreach的速度要慢很多。
我有证据。这些是我的发现
我使用下面的简单分析器来测试它们的性能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static void Main(string[] args) { DateTime start = DateTime.Now; List<string> names = new List<string>(); Enumerable.Range(1, 1000).ToList().ForEach(c => names.Add("Name =" + c.ToString())); for (int i = 0; i < 100; i++) { //For the for loop. Uncomment the other when you want to profile foreach loop //and comment this one //for (int j = 0; j < names.Count; j++) // Console.WriteLine(names[j]); //for the foreach loop foreach (string n in names) { Console.WriteLine(n); } } DateTime end = DateTime.Now; Console.WriteLine("Time taken =" + end.Subtract(start).TotalMilliseconds +" milli seconds"); |
我得到了以下结果
所用时间=11320.73毫秒(循环)
所用时间=11742.3296毫秒(foreach循环)
说到干净的代码,foreach语句要比for语句快得多!
LINQ(在C中)可以做很多相同的事情,但是新手开发人员往往很难阅读它们!
只是想补充一下,任何认为foreach被翻译成for,因而没有性能差异的人都是完全错误的。在引擎盖下会发生许多事情,例如对象类型的枚举,它不在简单的for循环中。它看起来更像一个迭代器循环:
1 2 3 4 5 | Iterator iter = o.getIterator(); while (iter.hasNext()){ obj value = iter.next(); ...do something } |
号
这与简单的for循环明显不同。如果你不明白原因,那么查找vtables。此外,谁知道hasNext函数中有什么?据我们所知,可能是:
1 2 3 | while (!haveiwastedtheprogramstimeenough){ } now advance |
号
除此之外,还有未知实现和效率的函数被调用。由于编译器不优化交叉函数边界,所以这里没有进行优化,只是简单的vtable查找和函数调用。这不仅仅是理论上的,在实践中,我已经看到了从forach转换到标准C arraylist的显著加速。公平地说,这是一个大约30000件物品的阵列列表,但仍然如此。