我为什么要使用foreach而不是for(int i = 0; i

时间:2010-12-08 00:57:31

标签: c# java .net loops foreach

在C#和Java中循环的好方法似乎是使用foreach而不是C style for循环。

我有理由比C风格更喜欢这种风格吗?

我对这两种情况特别感兴趣,但请解决您需要解释问题的案例。

  • 我希望对列表中的每个项目执行操作。
  • 我在列表中搜索某个项目,并希望在找到该项目时退出。

18 个答案:

答案 0 :(得分:78)

想象一下,你是一家餐馆的主厨,而且你们都在为自助餐准备一个巨大的煎蛋卷。你把一打十几个鸡蛋的纸盒交给两个厨房工作人员,然后告诉他们要开裂了。

第一个设置一个碗,打开箱子,依次抓住每个鸡蛋 - 从左到右穿过顶排,然后是底排 - 将它打开碗侧面,然后将其倒入碗中。最终他没蛋了。干得好。

第二个设置一个碗,打开箱子,然后破灭以获得一张纸和一支笔。他将数字0到11写在鸡蛋盒的隔间旁边,数字0写在纸上。他看着纸上的数字,找到标记为0的隔间,取出鸡蛋并将其打入碗中。他再次看着纸上的0,认为“0 + 1 = 1”,越过0并在纸上写1。他从隔间1抓起鸡蛋并将其弄碎。等等,直到12号纸上,他知道(不看!)没有更多的鸡蛋。干得好。

你认为第二个人头脑中有点乱,对吗?

使用高级语言工作的重点是避免用计算机的术语描述事物,并能够用自己的术语来描述它们。语言越高,这就越真实。在循环中递增计数器会分散您真正想要做的事情:处理每个元素。


除此之外,链接列表类型结构无法通过递增计数器和索引来有效处理:“索引”意味着从头开始计数。在C中,我们可以通过使用循环“计数器”的指针并解除引用来处理我们自己制作的链表。我们可以使用“迭代器”在现代C ++(以及在C#和Java中)中执行此操作,但这仍然存在间接性问题。


最后,有些语言足够高,实际上编写循环以“对列表中的每个项目执行操作”或“搜索列表中的项目”的想法是令人震惊(就像主厨不应该告诉第一个厨房工作人员如何确保所有鸡蛋都破裂一样)。提供了设置该循环结构的函数,并通过高阶函数或者在搜索情况下的比较值告诉它们 - 在循环中做什么。 (事实上​​,你可以在C ++中做这些事情,虽然界面有些笨拙。)

答案 1 :(得分:71)

我能想到的两个主要原因是:

1)它从底层容器类型中抽象出来。这意味着,例如,当您更改容器时,您不必更改循环容器中所有项目的代码 - 您要指定“执行此操作”的目标容器中的每个项目“,而不是表示

2)它消除了逐个错误的可能性。

在对列表中的每个项目执行操作方面,只需说出:

for(Item item: lst)
{
  op(item);
}

它完全向读者表达了意图,而不是手动处理迭代器。同样可以搜索物品。

答案 2 :(得分:37)

  • foreach更简单,更易读

  • 对于像链接列表

  • 这样的结构可能更有效
  • 并非所有馆藏都支持随机访问;迭代HashSet<T>Dictionary<TKey, TValue>.KeysCollection的唯一方法是foreach

  • foreach允许您迭代方法返回的集合,而不需要额外的临时变量:

    foreach(var thingy in SomeMethodCall(arguments)) { ... }
    

答案 3 :(得分:19)

对我来说,一个好处就是不容易犯错误,比如

    for(int i = 0; i < maxi; i++) {
        for(int j = 0; j < maxj; i++) {
...
        }
    }

更新: 这是bug发生的一种方式。我总结一下

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

现在代码中至少有两个错误(如果我们混淆了maxi和maxj,则会有更多错误)只会被运行时错误检测到。如果你不编写测试......而且这是一段罕见的代码 - 这会让某些人陷入困境 - 非常糟糕。

这就是为什么将内循环提取到方法中是个好主意:

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

它更具可读性。

答案 4 :(得分:16)

在所有情景[1]中,

foreach的效果与for完全相同,包括您所描述的直截了当的情况。

但是,与foreach相比,for具有某些与性能无关的优势:

  1. <强>便利即可。你不需要保留额外的本地i(除了促进循环之外没有任何其他目的),并且你不需要自己将当前值提取到变量中;循环结构已经处理好了。
  2. <强>一致性即可。使用foreach,您可以轻松地迭代不是数组的序列。如果你想使用for循环遍历非数组有序序列(例如地图/字典),那么你必须以不同的方式编写代码。 foreach在其涵盖的所有情况下均相同。
  3. 安全即可。拥有权利的同时也被赋予了重大的责任。如果你不首先需要增加循环变量,为什么要提供与增加循环变量相关的bug的机会呢?
  4. 正如我们所看到的,foreach大多数情况下使用“更好”。

    也就是说,如果你需要i的值用于其他目的,或者你正在处理一个你知道的数组结构(并且有一个实际的特定原因,它是一个数组),功能越来越强,金属for提供的功能将越来越多。

    [1]“在所有场景中”实际上意味着“所有场景中集合都是友好的迭代”,这实际上是“大多数场景”(见下面的评论)。我真的认为必须设计一个涉及迭代不友好集合的迭代场景。

答案 5 :(得分:10)

如果您将C#作为一种语言,那么您也应该考虑使用LINQ,因为这是进行循环的另一种合理方式。

通过对列表中的每个项目执行操作是指在列表中修改它,还是只是对项目执行某些操作(例如打印,累积,修改等) 。)?我怀疑它是后者,因为C#中的foreach不允许你修改你正在循环的集合,或者至少不方便...

这是两个简单的构造,首先使用for然后使用foreach,它访问列表中的所有字符串并将它们转换为大写字符串:

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中使用结束条件i < list.Count而不是i < length与一些预计算机length常量被认为是一种很好的做法,因为编译器无论如何都必须检查在循环中调用list[i]时的上限;如果我的理解是正确的,编译器在某些情况下可以优化它通常已经完成的上限检查。

以下是foreach等效词:

List<string> list = ...;
List<string> uppercase = new List<string> ();
foreach (name in list)
{
    uppercase.Add (name.ToUpper ());
}

注意:基本上,foreach构造可以迭代C#中的任何IEnumerableIEnumerable<T>,而不仅仅是数组或列表。因此,集合中的元素数量可能事先不知道,或者甚至可能是无限的(在这种情况下,您肯定必须在循环中包含一些终止条件,否则它将不会退出)。

以下是我能想到的一些等效解决方案,使用C#LINQ表示(并引入了 lambda表达式的概念,基本上是一个内联函数,它采用x并返回{以下示例中的{1}}:

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

或者如果您不介意将结果作为List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ()).ToList (); (可枚举的字符串集合),您可以删除IEnumerable<string>

ToList

或者可能是另一个使用C#SQL List<string> list = ...; var uppercase = list.Select (x => x.ToUpper ()); from关键字的关键字,它们是完全等效的:

select

LINQ非常富有表现力,我经常觉得代码比普通循环更具可读性。

您的第二个问题,在列表中搜索项目,并希望在找到该项目时退出也可以使用LINQ非常方便地实现。以下是List<string> list = ...; var uppercase = from name in list select name => name.ToUpper (); 循环的示例:

foreach

这是简单的LINQ等价物:

List<string> list = ...;
string result = null;
foreach (name in list)
{
    if (name.Contains ("Pierre"))
    {
        result = name;
        break;
    }
}

或使用查询语法:

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方法时它

我希望这为FirstOrDefaultfor辩论带来更多背景,至少在.NET世界中。

答案 6 :(得分:6)

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所说,“抽象是有选择性的无知”;如果您不需要知道如何迭代某些集合的详细信息,那么找到一种方法来忽略这些细节,并且您将编写更强大的代码。

答案 7 :(得分:4)

使用foreach的原因:

  • 它可以防止错误蔓延(例如,您忘记在for循环中i++),这可能导致循环出现故障。有许多方法可以搞定循环,但没有多少方法可以搞定foreach循环。
  • 它看起来更清洁/更不神秘。
  • 在某些情况下甚至可能无法使用for循环(例如,如果您有IEnumerable<T>,则无法像IList<T>那样编制索引。

使用for的原因:

  • 这些循环在迭代平面列表(数组)时具有轻微的性能优势,因为使用枚举器不会产生额外的间接级别。 (但是,这种性能提升很小。)
  • 您要枚举的对象未实现IEnumerable<T> - foreach仅对枚举进行操作。
  • 其他特殊情况;例如,如果要从一个数组复制到另一个数组,foreach将不会为您提供可用于寻址目标数组插槽的索引变量。在这种情况下,for是唯一有意义的事情。

你在问题​​中列出的两个案例在使用任何一个循环时实际上是相同的 - 在第一个中,你只是一直迭代到列表的末尾,而在第二个你break;一旦你有找到了你要找的东西。

进一步解释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();
}

答案 8 :(得分:3)

如果要迭代实现IEnumerable的集合,则使用 foreach 更自然,因为迭代中的下一个成员是在完成到达结束的测试的同时分配的。如,

 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];

答案 9 :(得分:2)

Java包含您指向的两种循环类型。您可以根据需要使用任何for循环变体。您的需求可以是这样的

  1. 您想要重新运行列表中搜索项的索引。
  2. 您想要获得该项目。
  3. 在第一种情况下,您应该使用经典(c样式)for循环。但在第二种情况下,您应该使用foreach循环。

    foreach循环也可用于第一种情况。但在这种情况下,您需要维护自己的索引。

答案 10 :(得分:1)

如果您可以使用foreach执行所需操作,请使用它;如果不是 - 例如,如果由于某种原因你需要索引变量 - 那么使用for。简单!

(使用forforeach同样可以实现两种方案。)

答案 11 :(得分:1)

我能想到几个原因

  1. can't mess up indexes,同样在移动环境中,你没有编译器优化,而且糟糕的写入for循环可以做几个bounderay检查,其中每个循环只做1次。
  2. can't change data input size(添加/删除元素)迭代它。你的代码不会轻易制动。如果需要过滤或转换数据,请使用其他循环。
  3. 您可以迭代数据结构,这些数据结构不能通过索引进行访问,但可以进行爬网。对于每个只需要实现iterable interface(java)或扩展IEnumerable(c#)。
  4. 你可以拥有较小的boiler plate,例如在解析XML时它与SAX和StAX之间的区别,首先需要内存中的DOM副本来引用一个元素,后者只是迭代数据(它不是那么快) ,但它具有记忆效率)
  5. 请注意,如果您要在列表中搜索每个项目,则很可能是错误地执行了此操作。请考虑使用hashmapbimap一起跳过搜索。

    假设程序员希望使用迭代器将for loop用作for each,则存在跳过元素的常见错误。所以在那个场景中它更安全。

    for ( Iterator<T> elements = input.iterator(); elements.hasNext(); ) {
       // Inside here, nothing stops programmer from calling `element.next();` 
       // more then once.
    } 
    

答案 12 :(得分:1)

至少在java中不使用foreach的一个原因是它将创建一个最终将被垃圾收集的迭代器对象。因此,如果您尝试编写避免垃圾回收的代码,最好避免使用foreach。但是,我相信纯数组是可以的,因为它不会创建迭代器。

答案 13 :(得分:0)

看起来大多数项目都被覆盖了...以下是我没有看到的有关您具体问题的一些额外说明。这些是硬性规则,而不是风格偏好的方式:

  

我希望对列表中的每个项目执行操作

foreach循环中,您无法更改迭代变量的值,因此,如果您要更改列表中特定项的值,则必须使用for。< / p>

值得注意的是,“酷”的方式现在是使用LINQ;如果您有兴趣,可以搜索大量资源。

答案 14 :(得分:0)

谈论干净的代码,foreach语句比for语句更快阅读!

Linq(在C#中)可以做同样的事情,但新手开发人员往往很难阅读它们!

答案 15 :(得分:0)

对于实施大量收集,foreach的速度要慢一些。

我有证据。这些是我的发现

我使用以下简单的探查器来测试他们的表现

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循环)

答案 16 :(得分:-1)

如果您通过更改枚举的集合,foreach也会通知您(即您在集合中有7个项目......直到另一个线程上的另一个操作删除了一个,现在您只有6个@_ @)

答案 17 :(得分:-2)

只是想补充一点,那些认为foreach被翻译成人并因此没有性能差异的人是错误的。在引擎盖下发生了很多事情,即对象类型的枚举,它不是简单的for循环。它看起来更像是一个迭代器循环:

Iterator iter = o.getIterator();
while (iter.hasNext()){
     obj value = iter.next();
     ...do something
}

与简单的for循环明显不同。如果你不明白为什么,那么查找vtables。此外,谁知道hasNext函数中有什么?我们所知道的可能是:

while (!haveiwastedtheprogramstimeenough){
}
now advance

除了Exageration之外,还有一个未知的实现功能和效率被调用。由于编译器不会优化跨越函数的边界,因此这里没有优化,只需要简单的vtable查找和函数调用。这不仅仅是理论,在实践中,通过从标准C#ArrayList上的foreach切换到for,我已经看到了显着的加速。公平地说,这是一个有大约30,000个项目的arraylist,但仍然。