为什么我应该使用foreach而不是for(int i = 0; i <length; i ++)?
看来在C#和Java循环中很酷的方式是使用foreach而不是C风格的循环。
有一个原因,我应该喜欢这种风格的C风格?
我对这两个案件特别感兴趣,但是请解决你需要解释的问题。
- 我希望对列表中的每个项目执行一个操作。
- 我正在search列表中的项目,并希望在find该项目时退出。
我能想到的两个主要原因是:
1)它从底层的容器types中抽象出来。 这意味着,例如,当您更改容器时,不必更改循环遍历容器中所有项目的代码 – 您正在指定“为容器中的每个项目执行此操作”的目标 ,不是手段 。
2)消除了错误的可能性。
就列表中的每个项目执行操作而言,直观地说:
for(Item item: lst) { op(item); }
它完美地向读者expression意图,而不是用迭代器手动做东西。 同上search项目。
想象一下,你是一家餐馆的主厨,而且你正在准备一个巨大的自助餐煎蛋。 你向两名厨房工作人员每人递一箱鸡蛋,并告诉他们从字面上开始。
第一个设置一个碗,打开箱子,依次抓住每个蛋 – 从左到右越过上排,然后下排 – 把它倒在碗的一边,然后倒入碗里。 最终他用完了鸡蛋。 工作做得很好
第二个人摆了一个碗,打开箱子,然后冲去拿一张纸和一支笔。 他在纸箱的隔间旁边写下数字0到11,并在纸上写下数字0。 他看着纸上的数字,发现标记为0的隔间,将鸡蛋取出并将其打入碗中。 他再看一下纸上的0,认为“0 + 1 = 1”,在纸上划出0并写1。 他从隔间1抓起鸡蛋并将其打开。 依此类推,直到12号纸在纸上,他知道(没有看!)鸡蛋不再有了。 工作做得很好
你会认为第二个人头脑有点混乱,对吧?
用高级语言工作的重点是避免用计算机的术语来描述事物,并且能够用自己的术语来描述事物。 语言越高级,这就越是真实。 在一个循环中增加一个计数器是从你真正想做的事情中分心:处理每个元素。
此外,链表types结构不能通过递增计数器和索引来有效地处理:“索引”意味着从头开始重新计数。 在C中,我们可以通过使用循环“counter”的指针来处理我们自己创build的链表,并对其进行解引用。 我们可以使用“迭代器”在现代C ++(以及C#和Java的程度)中做到这一点,但是仍然存在间接性问题。
最后,一些语言的级别足够高,以至于实际上编写循环来“对列表中的每个项目执行操作”或“search列表中的项目”的概念是令人震惊的(与主厨相同的方式不应该告诉第一厨房工作人员如何确保所有的鸡蛋都被破解)。 提供了设置该循环结构的函数,并且在search的情况下通过高阶函数或者比较值告诉它们 – 在循环中要做什么。 (事实上,你可以用C ++来做这些事情,尽pipe界面有些笨拙。)
-
foreach
更简单,更可读 -
对链接列表等结构可以更有效率
-
并非所有的集合都支持随机访问 迭代
HashSet<T>
或Dictionary<TKey, TValue>.KeysCollection
是foreach
。 -
foreach
允许你迭代一个方法返回的集合而不需要额外的临时variables:foreach(var thingy in SomeMethodCall(arguments)) { ... }
对我来说,一个好处是,犯这样的错误并不容易
for(int i = 0; i < maxi; i++) { for(int j = 0; j < maxj; i++) { ... } }
更新:这是错误发生的一种方式。 我做一笔款项
int sum = 0; for(int i = 0; i < maxi; i++) { sum += a[i]; }
然后决定更多地聚合它。 所以我把这个循环换成另一个。
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; }
编译失败,当然,所以我们手工编辑
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; }
现在代码中至less有两个错误(如果我们已经混淆了maxi和maxj),那么只有运行时错误才能检测到这个错误。 如果你不写testing…这是一个罕见的代码 – 这将咬别人 – 严重。
这就是为什么把内循环提取到一个方法中是个好主意:
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; }
而且更具可读性
在所有情况下, foreach
都会执行相同的操作[1],其中包括直接描述的操作。
但是, foreach
具有以下某些与性能无关的优势:
- 方便 。 你不需要保留一个额外的本地
i
(除了促进循环以外没有任何其他用途),你不需要自己将当前值取到variables中; 循环结构已经考虑到了这一点。 - 一致性 。 使用
foreach
,您可以轻松地迭代不是数组的序列。 如果你想使用循环非数组有序序列(例如地图/字典),那么你必须写一些不同的代码。 在所有情况下,foreach
都是一样的。 - 安全 。 拥有权利的同时也被赋予了重大的责任。 为什么打开机会增加循环variables相关的错误,如果你不需要它在第一个地方?
所以我们看到,在大多数情况下, foreach
是“更好”的。
也就是说,如果你需要将i
的值用于其他目的,或者如果你正在处理一个你知道是一个数组的数据结构(并且它实际上是一个数组的原因),增加的functionfor
提供的金属将是要走的路。
[1]“在所有情况下”的确意味着“所有情况下,收集是友好的迭代”,这实际上是“大多数情况下”(见下面的评论)。 但是我真的认为,一个迭代不友好的集合的迭代场景将不得不被devise。
如果你把C#作为一种语言,你应该考虑LINQ,因为这是做循环的另一个合乎逻辑的方法。
通过对列表中的每个项目执行一个操作,你的意思是修改它在列表中的位置,或者干脆对项目做一些事情(例如打印,累积,修改等)? 我怀疑这是后者,因为在C#中的foreach
不会允许你修改你正在循环的集合,或者至less不是以一种方便的方式…
这里有两个简单的构造,首先使用,然后使用foreach
,它访问列表中的所有string,并将其转换为大写string:
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 ()); }
(请注意,使用结束条件i < list.Count
而不是i < length
与一些i < list.Count
length
常量在.NET中被认为是一种好的做法,因为编译器无论如何不得不在调用list[i]
时检查上限在循环中;如果我的理解是正确的,那么编译器在某些情况下能够优化它通常会做的上限检查)。
这里是相同的foreach
:
List<string> list = ...; List<string> uppercase = new List<string> (); foreach (name in list) { uppercase.Add (name.ToUpper ()); }
注意:基本上, foreach
构造可以遍历C#中的任何IEnumerable
或IEnumerable<T>
,而不只是遍历数组或列表。 因此,集合中元素的数量可能不会事先知道,甚至可能是无限的(在这种情况下,您肯定会在循环中包含一些终止条件,或者不会退出)。
下面是我能想到的几个等价的解决scheme,用C#LINQ表示(它引入了lambdaexpression式的概念,基本上是一个内联函数,它接受x
并返回x.ToUpper ()
,如下例所示):
List<string> list = ...; List<string> uppercase = new List<string> (); uppercase.AddRange (list.Select (x => x.ToUpper ()));
或者使用由其构造函数填充的uppercase
列表:
List<string> list = ...; List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ()));
或者使用ToList
函数:
List<string> list = ...; List<string> uppercase = list.Select (x => x.ToUpper ()).ToList ();
还是types推理还是一样的:
List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ()).ToList ();
或者如果您不介意以IEnumerable<string>
(可枚举的string集合)的forms获取结果,则可以删除ToList
:
List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ());
或者可能是另一个与C#SQL类似的select
关键字,它们是完全等价的:
List<string> list = ...; var uppercase = from name in list select name => name.ToUpper ();
LINQ是非常富有performance力的,我经常觉得代码比纯循环更具可读性。
第二个问题, 在列表中search一个项目,并希望在find该项目时退出,也可以非常方便地使用LINQ实现。 这是一个foreach
循环的例子:
List<string> list = ...; string result = null; foreach (name in list) { if (name.Contains ("Pierre")) { result = name; break; } }
这是直接的LINQ等价物:
List<string> list = ...; string result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault ();
或使用查询语法:
List<string> list = ...; var results = from name in list where name.Contains ("Pierre") select name; string result = results.FirstOrDefault ();
results
枚举只能按需执行,这意味着只有在调用FirstOrDefault
方法时,列表才会被迭代,直到满足条件。
我希望这至less可以在.NET世界中带来更多的foreach
争论。
正如Stuart Golodetz所回答的,这是一个抽象概念。
如果你只使用i
作为索引,而不是使用i
的值为其他一些目的
String[] lines = getLines(); for( int i = 0 ; i < 10 ; ++i ) { System.out.println( "line " + i + lines[i] ) ; }
那么就不需要知道i
当前的价值,只能导致错误的可能性:
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)所说:“抽象是select性的无知”。 如果您不需要知道如何迭代某些集合的细节,那么可以find一种方法来忽略这些细节,然后编写更健壮的代码。
使用foreach的原因:
- 它可以防止错误蔓延(例如,您忘记了for循环中的
i++
),可能会导致循环出现故障。 有很多方法搞砸了循环,但没有太多的方法搞砸了foreach循环。 - 它看起来更清洁/不太神秘。
-
for
循环在某些情况下甚至是不可能的(例如,如果您有一个IEnumerable<T>
,它不能像IList<T>
那样进行索引)。
for
以下目的的理由:
- 这些types的循环在迭代平面列表(数组)时会有轻微的性能优势,因为使用枚举器不会创build额外的间接级别。 (但是,这个性能增益是最小的。)
- 您想要枚举的对象不实现
IEnumerable<T>
– 只对枚举操作。 - 其他专业情况; 例如,如果从一个数组拷贝到另一个数组,
foreach
不会给你一个索引variables,你可以用它来访问目标数组槽。for
是唯一在这种情况下才有意义的事情。
在你的问题中列出的两种情况在使用任一循环的时候都是相同的 – 首先,你只需迭代到列表的最后,在第二个循环中, 一旦你find了你正在寻找的物品。
只是为了进一步解释foreach
,这个循环:
IEnumerable<Something> bar = ...; foreach (var foo in bar) { // do stuff }
是语法糖:
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会更自然,因为迭代中的下一个成员是在testing完成的同时分配的。 例如,
foreach (string day in week) {/* Do something with the day ... */}
比…更直接
for (int i = 0; i < week.Length; i++) { day = week[i]; /* Use day ... */ }
您也可以在您自己的IEnumerable类的实现中使用for循环。 简单地让你的GetEnumerator()实现在循环的主体中使用C# yield关键字:
yield return my_array[i];
Java
拥有你所指向的两种循环types。 您可以根据需要使用for循环变体。 你的需求可以是这样的
- 您要重新运行列表中的search项目的索引。
- 你想获得物品本身。
在第一种情况下,你应该使用经典(C风格)循环。 但在第二种情况下,你应该使用foreach
循环。
foreach
循环也可以在第一种情况下使用。 但在这种情况下,你需要维护自己的索引。
如果你可以用foreach
来做你需要的东西然后使用它; 如果没有 – 例如,如果因为某些原因需要索引variables本身 – 然后for
。 简单!
(而且你的两个场景for
或者是foreach
都是同样可能的。)
至less在java中不使用foreach的一个原因是它会创build一个最终被垃圾收集的迭代器对象。 因此,如果您试图编写避免垃圾回收的代码,最好避免使用foreach。 不过,我相信纯数组是可以的,因为它不会创build迭代器。
我可以想到几个原因
- 你
can't mess up indexes
,在移动环境中,你也没有编译器优化,编写循环可以做几个bounderay检查,每个循环只有1个。 - 迭代它时,您
can't change data input size
(添加/删除元素)。 你的代码不容易制动。 如果您需要过滤或转换数据,则使用其他循环。 - 您可以迭代数据结构,这不能通过索引访问,但可以爬过。 对于每个只需要您实现
iterable interface
(java)或扩展IEnumerable
(c#)的。 - 你可以有更小的
boiler plate
,例如,当parsingXML时,它与SAX和StAX不同,首先需要DOM的内存中拷贝来引用后者只是迭代数据(它不是那么快,但它是有效的)
请注意,如果您正在search列表中的每个项目,则很可能是错误地执行了此操作。 考虑使用hashmap
或bimap
来一起跳过search。
假设程序员想使用for loop
for each
使用迭代器,那么存在跳过元素的常见错误。 所以在那个场景里更安全
for ( Iterator<T> elements = input.iterator(); elements.hasNext(); ) { // Inside here, nothing stops programmer from calling `element.next();` // more then once. }
谈论干净的代码,foreach语句比for语句要快得多。
Linq(在C#中)可以做很多相同的事情,但是新手开发者往往很难读懂它们!
它看起来像大多数项目都被覆盖…以下是一些额外的笔记,我没有看到提到有关您的具体问题。 这些是艰难的规则,而不是以某种方式偏好风格偏好:
我希望对列表中的每个项目执行一个操作
在foreach
循环中,你不能改变迭代variables的值,所以如果你想改变列表中特定项目的值,你必须使用for
。
同样值得注意的是,现在“酷”的方式是使用LINQ; 如果你有兴趣,你可以search很多资源。
对于实施重度收集来说,速度要慢几个数量级。
我有证据。 这些是我的发现
我使用以下简单的分析器来testing它们的性能
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毫秒(for循环)
所用时间= 11742.3296毫秒(foreach循环)
一个foreach
也会通知你,如果你正在通过修改来枚举的集合(也就是说,你的集合中有7个项目…直到另一个线程上的另一个操作移除了一个,现在你只有6 @ @ @)
只是想补充说,谁认为foreach被翻译成为,因此没有性能差异是错误的。 引擎盖下有很多事情,例如枚举不属于简单for循环的对象types。 它看起来更像是一个迭代器循环:
Iterator iter = o.getIterator(); while (iter.hasNext()){ obj value = iter.next(); ...do something }
这与简单的for循环显着不同。 如果你不明白为什么,那么查找vtable。 而且,谁知道hasNext函数是什么? 对于我们所知道的可能是:
while (!haveiwastedtheprogramstimeenough){ } now advance
抛开边界,有不明的实现和效率被称为function。 由于编译器不会对函数边界进行优化,因此这里没有任何优化发生,只是简单的vtable查找和函数调用。 这不仅仅是理论,在实践中,我已经看到从foreach切换到标准C#ArrayList显着加速。 公平地说,这是一个有三万件左右的数组列表,但是依然如此。