Boucle foreach avec rupture / retour vs boucle while avec invariant explicite et post-condition

17

C'est la façon la plus populaire (il me semble) de vérifier si une valeur est dans un tableau:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Cependant, dans un livre que j'ai lu il y a de nombreuses années, probablement, Wirth ou Dijkstra, il a été dit que ce style est meilleur (par rapport à une boucle while avec une sortie à l'intérieur):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

De cette façon, la condition de sortie supplémentaire devient une partie explicite de l'invariant de la boucle, il n'y a pas de conditions cachées et sort à l'intérieur de la boucle, tout est plus évident et plus d'une manière de programmation structurée. J'ai généralement préféré ce dernier modèle chaque fois que possible et forj'ai utilisé la boucle pour parcourir uniquement de aà b.

Et pourtant je ne peux pas dire que la première version soit moins claire. C'est peut-être encore plus clair et plus facile à comprendre, du moins pour les très débutants. Je me pose donc toujours la question de savoir laquelle est la meilleure?

Peut-être que quelqu'un peut donner une bonne justification en faveur d'une des méthodes?

Mise à jour: Il ne s'agit pas de points de retour de fonctions multiples, de lambdas ou de trouver un élément dans un tableau en soi. Il s'agit de savoir comment écrire des boucles avec des invariants plus complexes qu'une seule inégalité.

Mise à jour: OK, je vois l'intérêt des personnes qui répondent et commentent: j'ai mélangé la boucle foreach ici, qui elle-même est déjà beaucoup plus claire et lisible qu'une boucle while. Je n'aurais pas dû faire ça. Mais c'est aussi une question intéressante, alors laissons-la telle quelle: foreach-loop et une condition supplémentaire à l'intérieur, ou une boucle while avec un invariant de boucle explicite et une post-condition après. Il semble que la boucle foreach avec une condition et une sortie / pause soit gagnante. Je vais créer une question supplémentaire sans la boucle foreach (pour une liste chaînée).

Danila Piatov
la source
2
Les exemples de code cités ici mélangent plusieurs problèmes différents. Retours précoces et multiples (qui pour moi vont à la taille de la méthode (non illustrée)), recherche de tableau (qui demande une discussion impliquant des lambdas), foreach vs indexation directe ... Cette question serait plus claire et plus facile à répondez s'il se concentre sur une seule de ces questions à la fois.
Erik Eidt
1
Je sais que ce sont des exemples, mais il y a des langages qui ont des API pour gérer exactement ce cas d'utilisation. Iecollection.contains(foo)
Berin Loritsch
2
Vous voudrez peut-être trouver le livre et le relire maintenant pour voir ce qu'il a réellement dit.
Thorbjørn Ravn Andersen
1
"Mieux" est un mot très subjectif. Cela dit, on peut dire en un coup d'œil ce que fait la première version. Que la deuxième version fasse exactement la même chose demande un certain examen.
David Hammen

Réponses:

19

Je pense que pour les boucles simples, comme celles-ci, la première syntaxe standard est beaucoup plus claire. Certaines personnes considèrent que les retours multiples sont source de confusion ou d'une odeur de code, mais pour un morceau de code aussi petit, je ne pense pas que ce soit un vrai problème.

Cela devient un peu plus discutable pour les boucles plus complexes. Si le contenu de la boucle ne peut pas tenir sur votre écran et a plusieurs retours dans la boucle, il y a un argument à faire que les multiples points de sortie peuvent rendre le code plus difficile à maintenir. Par exemple, si vous deviez vous assurer qu'une méthode de maintenance d'état était exécutée avant de quitter la fonction, il serait facile de manquer de l'ajouter à l'une des instructions de retour et vous provoqueriez un bogue. Si toutes les conditions de fin peuvent être vérifiées dans une boucle while, vous n'avez qu'un seul point de sortie et pouvez ajouter ce code après.

Cela dit, avec des boucles en particulier, il est bon d'essayer de mettre autant de logique que possible dans des méthodes distinctes. Cela évite de nombreux cas où la deuxième méthode aurait des avantages. Les boucles Lean avec une logique clairement séparée auront plus d'importance que celui de ces styles que vous utilisez. De plus, si la plupart du code de base de votre application utilise un seul style, vous devez vous en tenir à ce style.

Nathanael
la source
56

C'est facile.

Rien n'est plus important que la clarté pour le lecteur. La première variante que j'ai trouvée incroyablement simple et claire.

La deuxième version «améliorée», j'ai dû lire plusieurs fois et m'assurer que toutes les conditions de bord étaient bonnes.

Il y a ZERO DOUBT qui est un meilleur style de codage (le premier est bien meilleur).

Maintenant - ce qui est CLAIR pour les gens peut varier d'une personne à l'autre. Je ne suis pas sûr qu'il existe des normes objectives pour cela (bien que publier sur un forum comme celui-ci et obtenir des contributions de diverses personnes peut aider).

Dans ce cas particulier, cependant, je peux vous dire pourquoi le premier algorithme est plus clair: je sais à quoi ressemble l'itération C ++ sur une syntaxe de conteneur. Je l'ai intériorisé. Quelqu'un UNFAMILIAR (sa nouvelle syntaxe) avec cette syntaxe pourrait préférer la deuxième variante.

Mais une fois que vous connaissez et comprenez cette nouvelle syntaxe, c'est un concept de base que vous pouvez simplement utiliser. Avec l'approche de l'itération de boucle (deuxième), vous devez soigneusement vérifier que l'utilisateur vérifie CORRECTEMENT toutes les conditions de bord pour boucler sur l'ensemble du tableau (par exemple, moins que au lieu de inférieur ou égal, même indice utilisé pour le test et pour l'indexation, etc.).

Lewis Pringle
la source
4
La nouveauté est relative, comme elle l'était déjà dans la norme 2011. De plus, la deuxième démo n'est évidemment pas C ++.
Déduplicateur
Une solution de rechange si vous souhaitez utiliser un seul point de sortie serait de mettre un drapeau longerLength = true, puis return longerLength.
Cullub
@Deduplicator Pourquoi la deuxième démo n'est-elle pas C ++? Je ne vois pas pourquoi ou je manque quelque chose d'évident?
Rakete1111
2
@ Rakete1111 Les tableaux bruts n'ont aucune propriété comme length. Si elle était effectivement déclarée comme un tableau et non comme un pointeur, ils pourraient utiliser sizeof, ou si c'était un std::array, la fonction membre correcte est size(), il n'y a pas de lengthpropriété.
IllusiveBrian
@IllusiveBrian: sizeofserait en octets ... Le plus générique puisque C ++ 17 l'est std::size().
Déduplicateur
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…] Tout est plus évident et plus d'une manière de programmation structurée.

Pas assez. La variable iexiste ici en dehors de la boucle while et fait donc partie de la portée externe, tandis que (jeu de mots voulu) xde la forboucle -lo n'existe que dans la portée de la boucle. La portée est un moyen très important d'introduire une structure dans la programmation.

nul
la source
1
@ruakh Je ne sais pas quoi retirer de votre commentaire. Il apparaît comme quelque peu passif-agressif, comme si ma réponse s'oppose à ce qui est écrit sur la page wiki. Veuillez développer.
null
La "programmation structurée" est un terme technique ayant une signification spécifique, et l'OP est objectivement correct que la version # 2 est conforme aux règles de la programmation structurée alors que la version # 1 ne l'est pas. D'après votre réponse, il semble que vous ne connaissiez pas le terme d'art et que vous l'interprétiez littéralement. Je ne sais pas pourquoi mon commentaire apparaît comme passif-agressif; Je le voulais simplement comme informatif.
ruakh
@ruakh Je ne suis pas d'accord avec le fait que la version # 2 soit plus conforme aux règles dans tous ses aspects et je l'ai expliqué dans ma réponse.
null
Vous dites "je ne suis pas d'accord" comme si c'était une chose subjective, mais ce n'est pas le cas. Le retour de l'intérieur d'une boucle est une violation catégorique des règles de programmation structurée. Je suis sûr que de nombreux amateurs de programmation structurée sont fans de variables à portée minimale, mais si vous réduisez la portée d'une variable en vous éloignant de la programmation structurée, alors vous vous êtes éloigné de la programmation structurée, période, et la réduction de la portée de la variable ne se défait pas cette.
ruakh
2

Les deux boucles ont une sémantique différente:

  • La première boucle répond simplement à une simple question oui / non: "Le tableau contient-il l'objet que je recherche?" Il le fait de la manière la plus brève possible.

  • La deuxième boucle répond à la question: "Si le tableau contient l'objet que je recherche, quel est l'index de la première correspondance?" Encore une fois, il le fait de la manière la plus brève possible.

Étant donné que la réponse à la deuxième question fournit strictement plus d'informations que la réponse à la première, vous pouvez choisir de répondre à la deuxième question, puis dériver la réponse de la première question. C'est ce que fait la ligne return i < array.length;, de toute façon.

Je pense qu'il est généralement préférable d' utiliser simplement l'outil qui correspond à l'objectif, sauf si vous pouvez réutiliser un outil déjà existant et plus flexible. C'est à dire:

  • L'utilisation de la première variante de la boucle est très bien.
  • Changer la première variante pour simplement définir une boolvariable et une pause est également très bien. (Évite la deuxième returninstruction, la réponse est disponible dans une variable au lieu d'un retour de fonction.)
  • L'utilisation std::findest très bien (réutilisation du code!).
  • Cependant, le codage explicite d'une recherche, puis la réduction de la réponse à un boolne l'est pas.
cmaster - réintégrer monica
la source
Ce serait bien si les downvoters laissaient un commentaire ...
cmaster - reinstate monica
2

Je proposerai une troisième option:

return array.find(value);

Il existe de nombreuses raisons différentes pour itérer sur un tableau: vérifiez si une valeur spécifique existe, transformez le tableau en un autre tableau, calculez une valeur agrégée, filtrez certaines valeurs hors du tableau ... Si vous utilisez une boucle simple pour, ce n'est pas clair en un coup d'œil spécifiquement comment la boucle for est utilisée. Cependant, la plupart des langages modernes ont des API riches sur leurs structures de données de tableau qui rendent ces différentes intentions très explicites.

Comparez la transformation d'un tableau en un autre avec une boucle for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

et en utilisant une mapfonction de style JavaScript :

array.map((value) => value * 2);

Ou sommer un tableau:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

contre:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Combien de temps vous faut-il pour comprendre ce que cela fait?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

contre

array.filter((value) => (value >= 0));

Dans les trois cas, bien que la boucle for soit certainement lisible, vous devez passer quelques instants pour comprendre comment la boucle for est utilisée et vérifier que tous les compteurs et les conditions de sortie sont corrects. Les fonctions modernes de style lambda rendent les objectifs des boucles extrêmement explicites, et vous savez avec certitude que les fonctions API appelées sont implémentées correctement.

La plupart des langages modernes, y compris JavaScript , Ruby , C # et Java , utilisent ce style d'interaction fonctionnelle avec des tableaux et des collections similaires.

En général, bien que je ne pense pas que l'utilisation de boucles soit nécessairement erronée, et c'est une question de goût personnel, je me suis retrouvé fortement en faveur de l'utilisation de ce style de travail avec des tableaux. Ceci est spécifiquement dû à la clarté accrue dans la détermination de ce que fait chaque boucle. Si votre langue possède des fonctionnalités ou des outils similaires dans ses bibliothèques standard, je vous suggère également d'envisager d'adopter ce style!

Kevin
la source
2
La recommandation array.findpose la question, car nous devons ensuite discuter de la meilleure façon de mettre en œuvre array.find. À moins que vous n'utilisiez du matériel avec une findopération intégrée , nous devons y écrire une boucle.
Barmar
2
@Barmar, je ne suis pas d'accord. Comme je l'ai indiqué dans ma réponse, de nombreux langages très utilisés fournissent des fonctions comme finddans leurs bibliothèques standard. Sans aucun doute, ces bibliothèques implémentent findet leurs proches utilisent des boucles for, mais c'est ce que fait une bonne fonction: elle abstrait les détails techniques loin du consommateur de la fonction, permettant au programmeur de ne pas avoir besoin de penser à ces détails. Ainsi, même s'il findest probablement implémenté avec une boucle for, il permet toujours de rendre le code plus lisible, et comme il est souvent dans la bibliothèque standard, son utilisation n'ajoute aucun frais ou risque significatif.
Kevin
4
Mais un ingénieur logiciel doit implémenter ces bibliothèques. Les mêmes principes d'ingénierie logicielle ne s'appliquent-ils pas aux auteurs de bibliothèques comme aux programmeurs d'applications? La question concerne l'écriture de boucles en général, et non la meilleure façon de rechercher un élément de tableau dans une langue particulière
Barmar
4
En d'autres termes, la recherche d'un élément de tableau n'est qu'un simple exemple qu'il a utilisé pour démontrer les différentes techniques de bouclage.
Barmar
-2

Tout se résume précisément à ce que l'on entend par «mieux». Pour les programmeurs pratiques, cela signifie généralement efficace - c'est-à-dire que dans ce cas, sortir directement de la boucle évite une comparaison supplémentaire et retourner une constante booléenne évite une comparaison en double; cela économise des cycles. Dijkstra est plus soucieux de rendre le code plus facile à prouver . [Il m'a semblé que l'éducation CS en Europe prend la «preuve de l'exactitude du code» beaucoup plus au sérieux que l'éducation CS aux États-Unis, où les forces économiques ont tendance à dominer les pratiques de codage]

PMar
la source
3
PMar, en termes de performances, les deux boucles sont à peu près équivalentes - elles ont toutes deux deux comparaisons.
Danila Piatov
1
Si l'on se souciait vraiment des performances, on utiliserait un algorithme plus rapide. par exemple trier le tableau et faire une recherche binaire, ou utiliser une table de hachage.
user949300
Danila - vous ne savez pas quelle structure de données se cache derrière cela. Un itérateur est toujours rapide. L'accès indexé peut être un temps linéaire, et la longueur n'a même pas besoin d'exister.
gnasher729