关于c#:为什么我应该在循环中使用foreach而不是for(int i = 0; i< length; i ++)?

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更简单易读。

  • 对于链表这样的结构,它可能更有效

  • 并非所有集合都支持随机访问;迭代HashSetDictionary.KeysCollection的唯一方法是foreach

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

    1
    foreach(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;
}

而且可读性更强。


foreach在所有场景[1]中的性能与for相同,包括您描述的直接场景。

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

  • 方便。您不需要保留一个额外的本地i(它在生活中除了促进循环之外没有其他目的),也不需要自己将当前值提取到变量中;循环构造已经处理了这一点。
  • 一致性。使用foreach可以轻松地迭代不是数组的序列。如果您想使用for循环一个非数组顺序序列(如映射/字典),那么您必须以稍微不同的方式编写代码。foreach在所有情况下都是相同的。
  • 安全。强大的力量带来巨大的责任。如果一开始不需要循环变量,为什么要为与增加循环变量相关的错误打开机会?
  • 如我们所见,在大多数情况下使用foreach更好。

    也就是说,如果您出于其他目的需要i的值,或者如果您正在处理一个您知道是数组的数据结构(并且有一个实际的特定原因使它成为数组),那么增加的功能(即更多地使用for提供的金属)将是前进的道路。

    [1]"在所有场景中"实际上是指"集合易于迭代的所有场景",这实际上是"大多数场景"(参见下面的注释)。然而,我真的认为涉及到一个不友好的迭代集合的迭代场景必须被设计出来。


    如果您将C作为一种语言作为目标,那么您可能也应该考虑使用LINQ,因为这是执行循环的另一种逻辑方法。

    通过对列表中的每个项目执行操作,您的意思是在列表中对其进行适当的修改,还是简单地对项目进行一些操作(例如打印、累积、修改等)?我怀疑是后者,因为C中的foreach不允许您修改正在循环使用的集合,或者至少不允许您以方便的方式…

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

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

    这是foreach等价物:

    1
    2
    3
    4
    5
    6
    List<string> list = ...;
    List<string> uppercase = new List<string> ();
    foreach (name in list)
    {
        uppercase.Add (name.ToUpper ());
    }

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

    这里有几个我能想到的等价的解决方案,用c linq表示(它引入了lambda表达式的概念,基本上是一个内联函数,采用x,并在下面的示例中返回x.ToUpper ())。

    1
    2
    3
    List<string> list = ...;
    List<string> uppercase = new List<string> ();
    uppercase.AddRange (list.Select (x => x.ToUpper ()));

    或者使用由其构造函数填充的uppercase列表:

    1
    2
    List<string> list = ...;
    List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ()));

    或者使用ToList功能:

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

    或者,如果您不介意得到一个IEnumerable的结果(字符串的可枚举集合),您可以删除ToList

    1
    2
    List<string> list = ...;
    var uppercase = list.Select (x => x.ToUpper ());

    或者另一个使用c sql的关键字,比如fromselect,完全等效:

    1
    2
    3
    List<string> list = ...;
    var uppercase = from name in list
                    select name => name.ToUpper ();

    Linq非常有表现力,而且经常,我觉得代码比简单的循环更具可读性。

    您的第二个问题,搜索列表中的一个项目,并希望在找到该项目时退出,也可以非常方便地使用LINQ实现。下面是一个foreach循环的示例:

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

    results枚举仅按需执行,这意味着在对其调用FirstOrDefault方法时,仅在满足条件之前有效地迭代列表。

    我希望这能给forforeach的辩论带来更多的背景,至少在.NET世界是这样。


    正如斯图亚特·戈洛德茨所回答的,这是一种抽象。

    如果您只使用i作为索引,而不是将i的值用于其他目的,例如

    1
    2
    3
    4
       String[] lines = getLines();
       for( int i = 0 ; i < 10 ; ++i ) {
          System.out.println("line" + i + lines[i] ) ;
       }

    那么就不需要知道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++),从而导致循环出现故障。有很多种方法可以让循环更糟糕,但是没有很多方法可以让每个循环更糟糕。
    • 它看起来更干净/不那么神秘。
    • 在某些情况下(例如,如果您有一个不能像IListcan那样被索引的IEnumerable),for循环甚至不可能实现。

    使用for的原因:

    • 在遍历平面列表(数组)时,这些类型的循环具有轻微的性能优势,因为不存在使用枚举器创建的额外间接级别。(然而,这种性能增益是最小的。)
    • 要枚举的对象不实现IEnumerable--foreach,只对可枚举对象进行操作。
    • 其他特殊情况;例如,如果要从一个数组复制到另一个数组,foreach将不会为您提供可用于寻址目标数组槽的索引变量。在这种情况下,for是唯一有意义的。

    在使用任何一个循环时,您在问题中列出的两个案例实际上都是相同的——首先,您只需迭代到列表的末尾,然后在第二个循环中,当您找到要查找的项目后,您将执行ecx1(14)。

    为了进一步解释foreach,这个循环:

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

    Java具有您所指的两种循环类型。您可以根据需要使用for循环变量。你的需求可以是这样的

  • 要重新运行列表中搜索项的索引。
  • 您想要获取项目本身。
  • 在第一种情况下,您应该使用经典的(C样式)for循环。但在第二种情况下,您应该使用foreach循环。

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


    我能想到几个原因

  • 在移动环境中,没有编译器优化,编写糟糕的for循环可以执行多个边界检查,其中每个循环只执行1。
  • 您可以在迭代时使用can't change data input size(添加/删除元素)。你的代码不会那么容易地刹车。如果需要过滤或转换数据,则使用其他循环。
  • 您可以迭代数据结构,这些数据结构不能被索引访问,但可以被爬行。对于每个只需要实现EDCOX1,2,(Java)或扩展EDCOX1,3(C)的需求。
  • 您可以使用较小的boiler plate,例如,在解析XML时,SAX和STAX之间的区别在于,首先需要内存中的DOM副本来引用元素,后者只是在数据上迭代(虽然速度不快,但具有内存效率)。
  • 请注意,如果您在列表中为每个项目搜索一个项目,那么很可能是做错事了。考虑使用hashmapbimap一起跳过搜索。

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

    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。但是,我相信对于纯数组来说是可以的,因为它不创建迭代器。


    如果您可以使用foreach,那么使用它;如果不能使用它,例如,如果出于某种原因需要索引变量本身,那么使用for。简单!

    (您的两种方案在使用forforeach时同样可行。)


    看起来大部分的东西都被覆盖了…以下是关于您的具体问题,我没有看到提到的一些额外注释。这些都是硬规则,与样式首选项不同:

    I wish to perform an operation on each item in a list

    foreach循环中,不能更改迭代变量的值,因此如果要更改列表中特定项的值,必须使用for

    值得注意的是,"酷"的方法现在是使用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还会通知您是否正在通过更改枚举的集合(即您的集合中有7个项……直到另一个线程上的另一个操作删除了一个,而现在您只有6@@)


    只是想补充一下,任何认为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件物品的阵列列表,但仍然如此。