web-dev-qa-db-fra.com

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

C'est la manière 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 plusieurs 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 autant que possible et utilisé la boucle for- pour itérer 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. Donc je me pose toujours la question de 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 fonction 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).

17
Danila Piatov

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 ne pas 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 seconde 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.

19
Nathanael

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 Edge étaient correctes.

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 la publication sur un forum comme celui-ci et l'obtention d'une variété de contributions de personnes puissent aider).

Dans ce cas particulier, cependant, je peux vous expliquer 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 Edge pour boucler sur l'ensemble du tableau (par exemple, moins que au lieu de moins ou égal, même indice utilisé pour le test et pour l'indexation, etc.).

56
Lewis Pringle
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 i existe ici en dehors de la boucle while et fait donc partie de la portée externe, tandis que (jeu de mots voulu) x de la boucle for- 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.

9
null

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 fonction map 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++;
  }
}

versus

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 les tableaux et 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 trouvé 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 d'envisager d'adopter également ce style!

2
Kevin

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. Voilà ce que la ligne return i < array.length; le fait de toute façon.

Je crois qu'il est généralement préférable de simplement tilisez l'outil qui convient à l'objectif à moins que vous ne puissiez réutiliser un outil déjà 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 variable bool et break est également très bien. (Évite la deuxième instruction return, la réponse est disponible dans une variable au lieu d'un retour de fonction.)
  • En utilisant std::find est très bien (réutilisation du code!).
  • Cependant, le codage explicite d'une recherche et la réduction de la réponse à un bool ne le sont pas.