Il semble que la façon cool de boucler en C # et Java consiste à utiliser foreach au lieu du style C pour les boucles.
Y a-t-il une raison pour laquelle je préférerais ce style au style C?
Je suis particulièrement intéressé par ces deux cas, mais veuillez aborder autant de cas que nécessaire pour expliquer vos points.
Je peux penser à deux raisons principales:
1) Il s'éloigne du type de conteneur sous-jacent. Cela signifie, par exemple, que vous n'avez pas à modifier le code qui boucle sur tous les éléments du conteneur lorsque vous changez le conteneur - vous spécifiez le objectif de "faire cela pour chaque élément dans le conteneur ", pas le signifie.
2) Il élimine la possibilité d'erreurs au coup par coup.
En termes d'exécution d'une 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 du lecteur, par opposition à faire manuellement des choses avec des itérateurs. Idem pour la recherche d'articles.
Imaginez que vous êtes le chef de cuisine d'un restaurant et que vous préparez tous une énorme omelette pour un buffet. Vous remettez un carton d'une douzaine d'œufs à chacun des deux membres du personnel de cuisine et leur dites de se casser, littéralement.
Le premier met en place un bol, ouvre la caisse, attrape chaque œuf à son tour - de gauche à droite à travers la rangée du haut, puis la rangée du bas - le cassant contre le côté du bol et le vidant ensuite dans le bol. Finalement, il manque d'œufs. Un travail bien fait.
Le second met en place un bol, ouvre la caisse, puis se précipite pour obtenir un morceau de papier et un stylo. Il écrit les chiffres de 0 à 11 à côté des compartiments du carton d'oeufs et le numéro 0 sur le papier. Il regarde le numéro sur le papier, trouve le compartiment étiqueté 0, enlève l'œuf et le casse dans le bol. Il regarde à nouveau le 0 sur le papier, pense "0 + 1 = 1", raye le 0 et écrit 1 sur le papier. Il attrape l'oeuf du compartiment 1 et le casse. Et ainsi de suite, jusqu'à ce que le chiffre 12 soit sur le papier et qu'il sache (sans regarder!) Qu'il n'y a plus d'oeufs. Un travail bien fait.
On pourrait penser que le deuxième gars était un peu foiré dans la tête, non?
Le but de travailler dans un langage de haut niveau est d'éviter d'avoir à décrire les choses en termes informatiques et de pouvoir les décrire dans ses propres termes. Plus la langue est élevée, 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 liée ne peuvent pas être traitées efficacement en incrémentant un compteur et en l'indexant: "indexer" signifie recommencer le comptage depuis le début. En C, nous pouvons traiter une liste chaînée que nous avons faite nous-mêmes en utilisant un pointeur pour le "compteur" de boucle et en le déréférençant. Nous pouvons le faire en C++ moderne (et dans une certaine mesure en C # et Java) en utilisant des "itérateurs", mais cela souffre toujours du problème de l'indirectité.
Enfin, certaines langues ont un niveau suffisamment élevé pour que l'idée de réellement écrire une boucle "effectuer une opération sur chaque élément d'une liste" ou "rechercher un élément dans une liste" soit épouvantable (dans de la même manière que le chef ne devrait pas avoir à dire au premier membre du personnel de cuisine comment s'assurer que tous les œufs sont fêlés). Des fonctions sont fournies pour configurer cette structure de boucle, et vous leur dites - via une fonction d'ordre supérieur, ou peut-être une valeur de comparaison, dans le cas de recherche - ce qu'il faut faire dans la boucle. (En fait, vous pouvez faire ces choses en C++, bien que les interfaces soient quelque peu maladroites.)
foreach
est plus simple et plus lisible
Il peut être plus efficace pour les constructions comme les listes chaînées
Toutes les collections ne prennent pas en charge l'accès aléatoire; la seule façon d'itérer un HashSet<T>
ou un Dictionary<TKey, TValue>.KeysCollection
est foreach
.
foreach
vous permet de parcourir une collection retourné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 comme
for(int i = 0; i < maxi; i++) {
for(int j = 0; j < maxj; i++) {
...
}
}
MISE À JOUR: C'est une façon dont le bogue se produit. Je fais une somme
int sum = 0;
for(int i = 0; i < maxi; i++) {
sum += a[i];
}
puis décidez de l'agréger davantage. J'enroule donc la boucle dans une 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;
}
La compilation échoue, bien sûr, nous modifions donc manuellement
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 c'est un morceau de code rare - cela mordra mal quelqu'un d'autre -.
C'est pourquoi c'est une bonne idée 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
dans tous les scénarios [1], y compris les plus simples comme ceux que vous décrivez.
Cependant, foreach
présente certains avantages non liés aux performances par rapport à for
:
i
local supplémentaire autour (qui n'a d'autre but dans la vie que de faciliter la boucle), et vous n'avez pas besoin de récupérer la valeur actuelle dans une variable vous-même; la construction de boucle s'est déjà occupée de cela.foreach
, vous pouvez parcourir les séquences qui ne sont pas des tableaux avec la même facilité. Si vous souhaitez utiliser for
pour parcourir une séquence ordonnée non tableau (par exemple une carte/dictionnaire), vous devez écrire le code un peu différemment. foreach
est le même dans tous les cas qu'il couvre.Donc, comme nous le voyons, foreach
est "mieux" à 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 que vous savez être un tableau (et qu'il existe une raison spécifique précise pour qu'il s'agisse d'un tableau), l'augmentation les fonctionnalités que les offres les plus simples for
seront la voie à suivre.
[1] "Dans tous les scénarios" signifie vraiment "tous les scénarios où la collection est conviviale pour être itérée", ce qui serait en fait "la plupart des scénarios" (voir les commentaires ci-dessous). Je pense vraiment qu'un scénario d'itération impliquant une collection hostile aux itérations devrait cependant être conçu.
Vous devriez probablement considérer également LINQ si vous ciblez C # comme langage, car c'est une autre façon logique de faire des boucles.
Par effectuer une opération sur chaque élément d'une liste voulez-vous 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 soupçonne que c'est le dernier, car foreach
en C # ne vous permettra pas de modifier la collection que vous parcourez, ou du moins pas de manière pratique ...
Voici deux constructions simples, utilisant d'abord for
puis foreach
, qui visitent toutes les chaînes d'une liste et les transforment en chaînes majuscules:
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 ());
}
(notez que l'utilisation de la condition de fin i < list.Count
au lieu de i < length
avec une constante de pré-ordinateur length
est considérée comme une bonne pratique dans .NET, car le compilateur devra de toute façon vérifier la borne supérieure lorsque list[i]
est invoqué dans la boucle; si ma compréhension est correcte, le compilateur est en mesure, dans certaines circonstances, d'optimiser la vérification de limite supérieure qu'il aurait normalement effectuée).
Voici l'équivalent foreach
:
List<string> list = ...;
List<string> uppercase = new List<string> ();
foreach (name in list)
{
uppercase.Add (name.ToUpper ());
}
Remarque: fondamentalement, la construction foreach
peut itérer sur n'importe quel IEnumerable
ou IEnumerable<T>
En C #, pas seulement sur des tableaux ou des listes. Le nombre d'éléments dans la collection peut donc ne pas être connu à l'avance, ou peut-être même infini (auquel cas vous devrez certainement inclure une condition de terminaison dans votre boucle, sinon elle ne se fermera pas).
Voici quelques solutions équivalentes auxquelles je peux penser, exprimées en utilisant C # LINQ (et qui introduisent le concept d'un expression lambda, essentiellement une fonction en ligne prenant une fonction x
et retournant x.ToUpper ()
dans les exemples suivants):
List<string> list = ...;
List<string> uppercase = new List<string> ();
uppercase.AddRange (list.Select (x => x.ToUpper ()));
Ou avec la liste uppercase
renseignée par son constructeur:
List<string> list = ...;
List<string> uppercase = new List<string> (list.Select (x => x.ToUpper ()));
Ou la même chose en utilisant la fonction ToList
:
List<string> list = ...;
List<string> uppercase = list.Select (x => x.ToUpper ()).ToList ();
Ou toujours la même chose avec l'inférence de type:
List<string> 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'un IEnumerable<string>
(une collection énumérable de chaînes), vous pouvez supprimer le ToList
:
List<string> list = ...;
var uppercase = list.Select (x => x.ToUpper ());
Ou peut-être un autre avec les mots clés de type C # SQL from
et select
, qui est entièrement équivalent:
List<string> list = ...;
var uppercase = from name in list
select name => name.ToUpper ();
LINQ est très expressif et très souvent, je pense que le code est plus lisible qu'une simple boucle.
Votre deuxième question, recherche d'un élément dans une liste, et souhaitez quitter lorsque cet élément est trouvé peut également être très facilement implémentée à l'aide de LINQ. Voici un exemple de boucle foreach
:
List<string> list = ...;
string result = null;
foreach (name in list)
{
if (name.Contains ("Pierre"))
{
result = name;
break;
}
}
Voici l'équivalent simple de LINQ:
List<string> list = ...;
string result = list.Where (x => x.Contains ("Pierre")).FirstOrDefault ();
ou avec la syntaxe de requête:
List<string> list = ...;
var results = from name in list
where name.Contains ("Pierre")
select name;
string result = results.FirstOrDefault ();
L'énumération results
n'est exécutée que à la demande, ce qui signifie que la liste ne sera itérée que jusqu'à ce que la condition soit remplie, lors de l'invocation de la méthode FirstOrDefault
dessus .
J'espère que cela apporte un peu plus de contexte au débat for
ou foreach
, au moins dans le monde .NET.
Comme l'a répondu Stuart Golodetz, c'est une abstraction.
Si vous utilisez uniquement i
comme index, au lieu d'utiliser la valeur de i
à d'autres fins comme
String[] 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 pouvoir 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 façon dont vous itérez une collection, trouvez un moyen d'ignorer ces détails et vous écrirez un code plus robuste.
Raisons d'utiliser foreach:
i++
dans la boucle for) qui pourrait entraîner un dysfonctionnement de la boucle. Il existe de nombreuses façons de visser pour les boucles, mais pas beaucoup de façons de visser pour chaque boucle.for
peut même ne pas être possible dans certains cas (par exemple, si vous avez un IEnumerable<T>
, qui ne peut pas être indexé comme un IList<T>
pouvez).Raisons d'utiliser for
:
IEnumerable<T>
- foreach
ne fonctionne que sur les énumérateurs.foreach
ne vous donnera pas de variable d'index que vous pouvez utiliser pour adresser l'emplacement de tableau de destination. for
est à peu près la seule chose qui a du sens dans de tels cas.Les deux cas que vous listez dans votre question sont effectivement identiques lorsque vous utilisez l'une ou l'autre boucle - dans le premier, vous répétez simplement jusqu'à la fin de la liste, et dans le second, vous break;
une fois que vous avez trouvé l'article que vous recherchez.
Juste pour expliquer foreach
plus loin, cette boucle:
IEnumerable<Something> bar = ...;
foreach (var foo in bar) {
// do stuff
}
est du sucre syntaxique pour:
IEnumerable<Something> bar = ...;
IEnumerator<Something> 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 est effectué. Par exemple.,
foreach (string 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 l'implémentation IEnumerable de votre classe. Demandez simplement à votre implémentation GetEnumerator () d'utiliser le mot clé C # yield dans le corps de votre boucle:
yield return my_array[i];
Java
possède les deux types de boucles que vous avez indiqués. Vous pouvez utiliser l'une des variantes de boucle for selon vos besoins. Votre besoin peut être comme ça
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 maintenir votre propre index.
Je pourrais penser à plusieurs raisons
can't mess up indexes
, également dans un environnement mobile, vous n'avez pas d'optimisations du compilateur et écrit en boucle pour mal pourrait faire plusieurs vérifications de limite, où comme pour chaque boucle ne fait que 1.can't change data input size
(ajouter/supprimer des éléments) lors de son itération. Votre code ne freine pas aussi facilement. Si vous devez filtrer ou transformer des données, utilisez d'autres boucles.iterable interface
(Java) ou étendez IEnumerable
(c #).boiler plate
, par exemple, lors de l'analyse de XML, c'est la différence entre SAX et StAX, il faut d'abord une copie en mémoire du DOM pour faire référence à un élément qui vient d'itérer sur les données (ce n'est pas aussi rapide, mais il est efficace en mémoire)Notez que si vous recherchez un élément dans la liste avec pour chacun, vous le faites très probablement mal. Pensez à utiliser hashmap
ou bimap
pour ignorer la recherche tous ensemble.
En supposant que le programmeur souhaite utiliser for loop
comme for each
à l'aide d'itérateurs, il existe un bogue courant de saut d'éléments. Donc, dans cette scène, c'est plus sûr.
for ( Iterator<T> elements = input.iterator(); elements.hasNext(); ) {
// Inside here, nothing stops programmer from calling `element.next();`
// more then once.
}
l'une des raisons de ne pas utiliser foreach au moins dans Java est qu'il créera un objet itérateur qui sera éventuellement récupéré. Par conséquent, si vous essayez d'écrire du code qui évite la récupération de place, il vaut mieux éviter foreach. Cependant, je pense que c'est correct pour les tableaux purs car il ne crée pas d'itérateur.
Si vous pouvez faire ce dont vous avez besoin avec foreach
alors utilisez-le; sinon - par exemple, si vous avez besoin de la variable d'index elle-même pour une raison quelconque - alors utilisez for
. Facile!
(Et vos deux scénarios sont également possibles avec for
ou foreach
.)
foreach est d'un ordre de grandeur plus lent pour la mise en œuvre d'une collecte intensive.
J'ai la preuve. Ce sont mes conclusions
J'ai utilisé le profileur simple suivant pour tester leurs performances
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");
Et j'ai obtenu les résultats suivants
Temps pris = 11320,73 milli secondes (pour boucle)
Temps pris = 11742,3296 milli secondes (pour chaque boucle)
Il semble que la plupart des articles soient couverts ... voici quelques notes supplémentaires que je ne vois pas mentionnées concernant vos questions spécifiques. Ce sont des règles strictes par opposition aux préférences de style dans un sens ou dans l'autre:
Je souhaite effectuer une opération sur chaque élément d'une liste
Dans une boucle foreach
, vous ne pouvez pas changer la valeur de la variable d'itération, donc si vous cherchez à changer la valeur d'un élément spécifique dans votre liste, vous devez utiliser for
.
Il convient également de noter que la méthode "cool" consiste à utiliser LINQ; il existe de nombreuses ressources que vous pouvez rechercher si vous êtes intéressé.
En parlant de code propre, une instruction foreach est beaucoup plus rapide à lire qu'une instruction for!
Linq (en C #) peut faire la même chose, mais les développeurs novices ont tendance à avoir du mal à les lire!