Pourquoi devrais-je utiliser foreach au lieu de for (int i = 0; i <length; i ++) dans les boucles?

Il semble que la méthode géniale de boucler en C # et en Java consiste à utiliser foreach au lieu du style C pour les boucles.

Y a-t-il une raison pour laquelle je devrais préférer ce style au style C?

Je suis particulièrement intéressé par ces deux cas, mais veuillez répondre à autant de cas que nécessaire pour expliquer vos points.

  • Je souhaite effectuer une opération sur chaque élément d’une liste.
  • Je recherche un élément dans une liste et souhaite quitter lorsque cet élément est trouvé.

Les deux principales raisons pour lesquelles je peux penser sont:

1) Il s’éloigne du type de conteneur sous-jacent. Cela signifie, par exemple, que vous n’avez pas besoin de modifier le code qui boucle sur tous les éléments du conteneur lorsque vous changez de conteneur – vous spécifiez l’ objective de “faire ceci pour chaque élément du conteneur”, pas les moyens .

2) Il élimine la possibilité d’erreurs par un seul.

En termes d’opération sur chaque élément d’une liste, il est intuitif de simplement dire:

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

Il exprime parfaitement l’intention au lecteur, par opposition à faire manuellement des trucs avec des iterators. Idem pour rechercher des articles.

Imaginez que vous êtes le chef cuisinier d’un restaurant et que vous préparez tous une énorme omelette pour un buffet. Vous remettez un carton d’une douzaine d’oeufs à chacun des deux membres du personnel de la cuisine et dites-leur de craquer, littéralement.

Le premier place un bol, ouvre la caisse, attrape chaque œuf à tour de rôle – de gauche à droite sur la rangée supérieure, puis la rangée du bas – le brise contre le côté du bol et le vide ensuite dans le bol. Il finit par manquer d’œufs. Un travail bien fait.

Le second installe un bol, ouvre la caisse et se précipite pour aller chercher un morceau de papier et un stylo. Il écrit les chiffres 0 à 11 à côté des compartiments de la boîte à œufs et le chiffre 0 sur le papier. Il regarde le numéro sur le papier, trouve le compartiment étiqueté 0, enlève l’oeuf et le fend dans le bol. Il regarde à nouveau le 0 sur le papier, pense “0 + 1 = 1”, biffe le 0 et écrit 1 sur le papier. Il attrape l’oeuf du compartiment 1 et le craque. Et ainsi de suite, jusqu’à ce que le numéro 12 soit sur le papier et qu’il sache (sans regarder!) Qu’il n’y a plus d’œufs. Un travail bien fait.

On pourrait penser que le deuxième gars était un peu foutu dans la tête, non?

Le but de travailler dans un langage de haut niveau est d’éviter d’avoir à décrire les choses dans les termes d’un ordinateur et de pouvoir les décrire dans vos propres termes. Plus le langage est haut, plus c’est vrai. L’incrémentation d’un compteur dans une boucle est une distraction de ce que vous voulez vraiment faire: traiter chaque élément.


De plus, les structures de type liste chaînée ne peuvent pas être traitées efficacement en incrémentant un compteur et en les indexant: “indexer” signifie commencer à compter depuis le début. En C, nous pouvons traiter une liste liée que nous avons nous-mêmes créée en utilisant un pointeur pour la boucle “counter” et en la déréférençant. Nous pouvons le faire en C ++ moderne (et dans une certaine mesure en C # et en Java) en utilisant des “iterators”, mais cela souffre toujours du problème d’indirectivité.


Enfin, certaines langues sont suffisamment élevées pour que l’idée d’ écrire une boucle pour “effectuer une opération sur chaque élément d’une liste” ou “rechercher un élément dans une liste” soit épouvantable (de la même manière que le chef cuisinier ne devrait pas avoir à dire au premier membre du personnel de cuisine comment faire en sorte que tous les œufs soient fissurés). Les fonctions fournies définissent cette structure de boucle et vous leur indiquez – via une fonction d’ordre supérieur, ou une valeur de comparaison, dans la requête – ce qu’il faut faire dans la boucle. (En fait, vous pouvez faire ces choses en C ++, bien que les interfaces soient un peu maladroites.)

  • foreach est plus simple et plus lisible

  • Il peut être plus efficace pour les constructions telles que les listes liées

  • Toutes les collections ne prennent pas en charge l’access aléatoire; Le seul moyen d’itérer un HashSet ou un Dictionary.KeysCollection est foreach .

  • foreach vous permet de parcourir une collection renvoyée par une méthode sans variable temporaire supplémentaire:

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

Un avantage pour moi est qu’il est moins facile de faire des erreurs telles que

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

MISE À JOUR: Ceci est une façon dont le bogue se produit. Je fais une sum

 int sum = 0; for(int i = 0; i < maxi; i++) { sum += a[i]; } 

puis décidez de l'agréger davantage. Donc je boucler la boucle dans un autre.

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

Comstackr échoue, bien sûr, nous modifions donc à la main

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

Il y a maintenant au moins DEUX erreurs dans le code (et plus si nous avons confondu maxi et maxj) qui ne seront détectées que par des erreurs d'exécution. Et si vous n'écrivez pas de tests ... et que c'est un morceau de code rare - cela va mordre quelqu'un d'autre - mal.

C'est pourquoi il est judicieux d'extraire la boucle interne dans une méthode:

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

et c'est plus lisible.

foreach fonctionnera de manière identique à un for tous les scénarios [1], y compris ceux simples que vous décrivez.

Cependant, foreach a certains avantages non liés à la performance par rapport for :

  1. Commodité . Vous n’avez pas besoin de conserver un i local supplémentaire (ce qui n’a aucun sens dans la vie, sauf pour faciliter la boucle), et vous n’avez pas besoin de chercher la valeur actuelle dans une variable vous-même; la construction de boucle a déjà pris soin de cela.
  2. Cohérence . Avec foreach , vous pouvez parcourir des séquences qui ne sont pas des tableaux avec la même facilité. Si vous voulez utiliser for faire une boucle sur une séquence non ordonnée par un tableau (par exemple, une carte / un dictionnaire), vous devez écrire le code un peu différemment. foreach est le même dans tous les cas.
  3. Sécurité Un grand pouvoir implique de grandes responsabilités. Pourquoi ouvrir des opportunités pour des bogues liés à l’incrémentation de la variable de boucle si vous n’en avez pas besoin en premier lieu?

Comme nous le voyons, foreach est “préférable” à utiliser dans la plupart des situations.

Cela dit, si vous avez besoin de la valeur de i à d’autres fins, ou si vous manipulez une structure de données dont vous savez qu’elle est un tableau (et qu’il existe une raison spécifique d’être un tableau), la fonctionnalité accrue Le to-the-metal for offres sera la voie à suivre.

[1] “Dans tous les scénarios” signifie vraiment “tous les scénarios où la collection est amicale à être itérée”, ce qui serait en fait “la plupart des scénarios” (voir les commentaires ci-dessous). Je pense toutefois qu’un scénario d’itération impliquant une collection inopérante devrait être mis au point.

Vous devriez probablement également considérer LINQ si vous ciblez C # comme langage, car il s’agit d’une autre manière logique de faire des boucles.

Par effectuer une opération sur chaque élément d’une liste, vous voulez dire le modifier en place dans la liste, ou simplement faire quelque chose avec l’élément (par exemple, l’imprimer, l’accumuler, le modifier, etc.)? Je pense que c’est le dernier, car foreach en C # ne vous permettra pas de modifier la collection sur laquelle vous bouclez, ou du moins pas d’une manière pratique …

Voici deux constructions simples, utilisant d’abord for , puis using foreach , qui visitent toutes les chaînes d’une liste et les transforment en chaînes majuscules:

 List list = ...; List uppercase = new List (); for (int i = 0; i < list.Count; i++) { string name = list[i]; uppercase.Add (name.ToUpper ()); } 

(Notez que l'utilisation de la condition de fin i < list.Count au lieu de i < length avec une constante de length pré-ordinateur est considérée comme une bonne pratique dans .NET, car le compilateur devrait de toute façon vérifier la limite supérieure lorsque la list[i] est appelée dans la boucle, si ma compréhension est correcte, le compilateur est capable dans certaines circonstances d'optimiser la vérification de la limite supérieure qu'il aurait normalement effectuée.

Voici l'équivalent de foreach :

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

Note: en gros, la construction foreach peut itérer sur tout IEnumerable ou IEnumerable en C #, pas seulement sur les tableaux ou les tableaux. Le nombre d'éléments dans la collection pourrait donc ne pas être connu à l'avance, ou même être infini (dans ce cas, vous devrez certainement inclure des conditions de terminaison dans votre boucle ou ne pas les quitter).

Voici quelques solutions équivalentes que je peux imaginer, exprimées en utilisant C # LINQ (et qui introduit le concept d'une expression lambda , essentiellement une fonction inline prenant x et renvoyant x.ToUpper () dans les exemples suivants):

 List list = ...; List uppercase = new List (); uppercase.AddRange (list.Select (x => x.ToUpper ())); 

Ou avec la liste uppercase remplie par son constructeur:

 List list = ...; List uppercase = new List (list.Select (x => x.ToUpper ())); 

Ou le même en utilisant la fonction ToList :

 List list = ...; List uppercase = list.Select (x => x.ToUpper ()).ToList (); 

Ou encore la même chose avec l'inférence de type:

 List list = ...; var uppercase = list.Select (x => x.ToUpper ()).ToList (); 

ou si cela ne vous dérange pas d'obtenir le résultat sous la forme d'une IEnumerable (une collection énumérable de chaînes), vous pouvez supprimer la liste de ToList :

 List list = ...; var uppercase = list.Select (x => x.ToUpper ()); 

Ou peut-être une autre avec des mots-clés from type C # SQL et select -la, ce qui est totalement équivalent:

 List list = ...; var uppercase = from name in list select name => name.ToUpper (); 

LINQ est très expressif et très souvent, j'estime que le code est plus lisible qu'une simple boucle.

Votre deuxième question, rechercher un élément dans une liste, et souhaiter quitter lorsque cet élément est trouvé peut également être très facilement implémentée en utilisant LINQ. Voici un exemple de boucle foreach :

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

Voici l'équivalent LINQ simple:

 List list = ...; ssortingng result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault (); 

ou avec la syntaxe de requête:

 List list = ...; var results = from name in list where name.Contains ("Pierre") select name; ssortingng result = results.FirstOrDefault (); 

L'énumération des results n'est exécutée qu'à la demande , ce qui signifie qu'effectivement, la liste ne sera itérée que lorsque la condition sera remplie, lors de l'appel de la méthode FirstOrDefault .

J'espère que cela apporte un peu plus de contexte au débat for ou for le monde, du moins dans le monde .NET.

Comme Stuart Golodetz a répondu, c’est une abstraction.

Si vous utilisez uniquement i comme index, par opposition à utiliser la valeur de i pour une autre raison

  Ssortingng[] lines = getLines(); for( int i = 0 ; i < 10 ; ++i ) { System.out.println( "line " + i + lines[i] ) ; } 

alors il n'y a pas besoin de connaître la valeur actuelle de i , et être capable de simplement conduire à la possibilité d'erreurs:

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

Comme le dit Andrew Koenig, "l'abstraction est une ignorance sélective"; Si vous n'avez pas besoin de connaître les détails de la manière dont vous effectuez une itération d'une collection, trouvez un moyen d'ignorer ces détails et vous écrirez un code plus robuste.

Raisons d’utiliser foreach:

  • Cela évite que des erreurs ne se glissent (par exemple, vous avez oublié i++ dans la boucle for), ce qui pourrait entraîner un dysfonctionnement de la boucle. Il y a beaucoup de façons de bousiller les boucles, mais pas beaucoup de façons de bousiller les boucles foreach.
  • Cela semble beaucoup plus propre / moins cryptique.
  • Une boucle for peut même ne pas être possible dans certains cas (par exemple, si vous avez un IEnumerable , qui ne peut pas être indexé comme un IList peut).

Raisons d’utiliser for :

  • Ces types de boucles présentent un léger avantage de performance lors d’une itération sur des listes plates (tableaux) car il n’y a pas de niveau d’indirection supplémentaire créé à l’aide d’un énumérateur. (Cependant, ce gain de performance est minime.)
  • L’object que vous souhaitez énumérer n’implémente pas IEnumerableforeach fonctionne uniquement sur les énumérables.
  • Autres situations spécialisées; Par exemple, si vous copiez d’un tableau à un autre, foreach ne vous donnera pas une variable d’index que vous pouvez utiliser pour adresser le logement de tableau de destination. for s’agit de la seule chose qui ait du sens dans de tels cas.

Les deux cas que vous répertoriez dans votre question sont effectivement identiques lorsque vous utilisez l’une ou l’autre des boucles – dans le premier cas, vous ne faites qu’itérer la fin de la liste et, dans le second, vous les break; une fois que vous avez trouvé l’article que vous recherchez.

Juste pour expliquer plus loin, cette boucle:

 IEnumerable bar = ...; foreach (var foo in bar) { // do stuff } 

est le sucre syntaxique pour:

 IEnumerable bar = ...; IEnumerator e = bar.GetEnumerator(); try { Something foo; while (e.MoveNext()) { foo = e.Current; // do stuff } } finally { ((IDisposable)e).Dispose(); } 

Si vous parcourez une collection qui implémente IEnumerable, il est plus naturel d’utiliser foreach car le membre suivant de l’itération est affecté en même temps que le test pour atteindre la fin. Par exemple,

  foreach (ssortingng day in week) {/* Do something with the day ... */} 

est plus simple que

 for (int i = 0; i < week.Length; i++) { day = week[i]; /* Use day ... */ } 

Vous pouvez également utiliser une boucle for dans la propre implémentation d'IEnumerable de votre classe. Votre implémentation GetEnumerator () utilise simplement le mot-clé de rendement C # dans le corps de votre boucle:

 yield return my_array[i]; 

Java possède les deux types de boucle que vous avez indiqués. Vous pouvez utiliser l’une ou l’autre des variantes de boucles en fonction de vos besoins. Votre besoin peut être comme ça

  1. Vous souhaitez réexécuter l’index de votre élément de recherche dans la liste.
  2. Vous voulez obtenir l’article lui-même.

Dans le premier cas, vous devez utiliser le classique (style c) pour la boucle. mais dans le second cas, vous devez utiliser la boucle foreach .

La boucle foreach peut également être utilisée dans le premier cas. mais dans ce cas, vous devez conserver votre propre index.

Si vous pouvez faire ce dont vous avez besoin avec foreach utilisez-le; sinon – par exemple, si vous avez besoin de la variable d’index elle-même pour une raison quelconque – alors utilisez for . Simple!

(Et vos deux scénarios sont également possibles avec soit for ou for .)

Une des raisons de ne pas utiliser foreach au moins en Java est qu’il créera un object iterator qui sera éventuellement récupéré. Ainsi, si vous essayez d’écrire du code qui évite la récupération de place, il est préférable d’éviter de le faire. Cependant, je pense que c’est correct pour les tableaux purs car il ne crée pas d’iterator.

Je pourrais penser à plusieurs raisons

  1. vous can't mess up indexes , même dans un environnement mobile, vous ne disposez pas d’optimisations du compilateur.
  2. vous can't change data input size (append / supprimer des éléments) lors de l’itération. Votre code ne freine pas facilement. Si vous devez filtrer ou transformer des données, utilisez d’autres boucles.
  3. vous pouvez parcourir les structures de données, qui ne peuvent pas être accessibles par index, mais peuvent être explorées. Pour chacun, il vous suffit d’implémenter une iterable interface (Java) ou d’étendre IEnumerable (c #).
  4. vous pouvez avoir une boiler plate plus petite, par exemple lors de l’parsing syntaxique de XML, c’est la différence entre SAX et StAX, le premier besoin de copie en mémoire du DOM pour renvoyer à un élément itère juste sur les données )

Notez que si vous recherchez un élément dans la liste avec pour chacun, vous le faites probablement de manière incorrecte. Pensez à utiliser hashmap ou bimap pour ignorer la recherche tous ensemble.

En supposant que le programmeur veuille utiliser for loop comme for each utilisation d’iterators, il existe un bug commun des éléments sautés. Donc, dans cette scène, c’est plus sûr.

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

En parlant de code propre, une déclaration foreach est beaucoup plus rapide à lire qu’une déclaration!

Linq (en C #) peut faire la même chose, mais les développeurs novices ont du mal à les lire!

Il semble que la plupart des éléments soient couverts … Voici quelques notes supplémentaires que je ne vois pas mentionnées concernant vos questions spécifiques. Ce sont des règles ssortingctes par opposition aux préférences de style d’une manière ou d’une autre:

Je souhaite effectuer une opération sur chaque élément d’une liste

Dans une boucle foreach , vous ne pouvez pas modifier la valeur de la variable d’itération. Par conséquent, si vous souhaitez modifier la valeur d’un élément spécifique de votre liste, vous devez l’utiliser.

Il convient également de noter que la méthode “cool” consiste maintenant à utiliser LINQ; il y a beaucoup de ressources que vous pouvez rechercher si vous êtes intéressé.