web-dev-qa-db-fra.com

Faites une mission à l'intérieur d'une condition considérée comme une odeur de code?

Plusieurs fois, je dois écrire une boucle nécessitant une initialisation d'une condition de boucle et une mise à jour à chaque fois que la boucle s'exécute. Voici un exemple:

List<String> currentStrings = getCurrentStrings();
while(currentStrings.size() > 0) {
  doThingsThatCanAlterCurrentStrings();
  currentStrings = getCurrentStrings();
}

Une chose que je n'aime pas à propos de ce code est l'appel en double à getCurrentStrings(). Une option consiste à ajouter de l'affectation à la condition comme suit:

List<String> currentStrings;
while( (currentStrings = getCurrentStrings()).size() > 0) {
  doThingsThatCanAlterCurrentStrings();
}

Mais pendant que je dispose maintenant de moins de duplication et de moins de code, je sens que mon code est maintenant plus difficile à lire. D'autre part, il est plus facile de comprendre que nous bougeons sur une variable pouvant être modifiée par la boucle.

Quelle est la meilleure pratique à suivre dans des cas comme celui-ci?

7
vainolo

Tout d'abord, je voudrais définitivement encadrer la première version en tant que boucle:

for (List<String> currentStrings = getCurrentStrings();
     currentStrings.size() > 0; // if your List has an isEmpty() prefer it
     currentStrings = getCurrentStrings()) {
  ...
}

Malheureusement, il n'y a pas de manière idiomatique en C++, Java ou C # que je connaisse de me débarrasser de la duplication entre initialisateur et incrémentation. J'aime personnellement abstraire le modèle de boucle dans un Iterable ou Enumerable ou quelle que soit votre langue fournit. Mais à la fin, cela déplace simplement la duplication dans un endroit réutilisable. Voici un exemple C #:

IEnumerable<T> ValidResults<T>(Func<T> grab, Func<bool, T> validate) {
  for (T t = grab(); validate(t); t = grab()) {
    yield return t;
  }
}
// != null is a common condition
IEnumerable<T> NonNullResults<T>(Func<T> grab) where T : class {
  return ValidResults(grab, t => t != null);
}

Maintenant, vous pouvez faire ceci:

foreach(var currentStrings in NonNullResults(getCurrentStrings)) {
  ...
}

C # 's yield fait écrire cela facile; C'est plus laids in Java ou C++.

La culture C++ accepte plus d'affectation in-conditions que les autres langues et les conversions booléennes implicites sont réellement utilisées dans certaines idiomes, par exemple. Type requêtes:

if (Derived* d = dynamic_cast<Derived*>(base)) {...}

Ce qui précède s'appuie sur la conversion implicite des pointeurs vers Bool et est idiomatique. Voici un autre:

std::string s;
while (std::getline(std::cin, s)) {...}

Cela modifie dans la condition.

Le modèle commun, cependant, est que la condition elle-même est triviale, comptant généralement complètement une conversion implicite à Bool. Étant donné que les collections ne font pas cela, mettre un test vide là-bas serait considéré comme moins idiomatique.

C Culture est encore plus acceptable, avec le fgetc boucle idiome ressemblant à ceci:

int c;
while((c = fgetc(stream)) != EOF) {...}

Mais dans des langues de niveau supérieur, cela est froncé, car le niveau supérieur est généralement d'une moindre acceptation du code délicat.

10
Sebastian Redl

Le problème fondamental ici, il me semble que vous avez un N plus une demi-boucle , et ceux-ci sont toujours un peu désordonnés à exprimer. Dans ce cas particulier, vous pourrait hander la partie "moitié" de la boucle dans le test, comme vous l'avez fait, mais cela me semble très maladroit . Deux façons d'exprimer cette boucle peuvent être:

Idiomatique C/C++ à mon avis:

for (;;) {
    List<String> currentStrings = getCurrentStrings();
    if (!currentStrings.size())
        break;
    doThingsThatCanAlterCurrentStrings();
}

Stricte "programmation structurée", qui tend à froncer les sourcils break:

for (bool done = false; !done; ) {
    List<String> currentStrings = getCurrentStrings();
    if (currentStrings.size() > 0) {
        doThingsThatCanAlterCurrentStrings();
    } else {
        done = true;
    }
}
4
microtherion

L'ancien code semble plus rationnel et lisible pour moi et toute la boucle a également un sens parfait. Je ne suis pas sûr du contexte du programme, mais il n'y a rien de mal à la structure de la boucle en substance.

L'exemple ultérieur semble essayer d'écrire un intelligent Code, qui me déroute absolument à moi dans le premier coup d'œil.

Je suis également d'accord avec rob y ---- Réponse que, à tout premier coup, vous pourriez penser que cela devrait être un équation== plutôt que an attribution Cependant, si vous lisez réellement le tandis que instruction à la fin, vous réaliserez que ce n'est pas une faute de frappe ou une erreur, mais le problème est que vous ne pouvez pas comprendre clairement pourquoi Il y a une affectation dans le tandis que Énoncé sauf si vous gardez le nom de la fonction exactement comme doThingsThatCanAlterCurrentStrings ou Ajouter un commentaire en ligne qui explique la fonction suivante est susceptible de modifier la valeur de currentStrings.

1
Mahdi

Ce type de construction apparaît lorsque vous faites une sorte de lecture tamponnée, où la lecture remplit un tampon et renvoie le nombre d'octets lus, ou 0. C'est une construction assez familière.

Il y a un risque que quelqu'un pourrait penser que vous avez eu = confondu avec ==. Vous pouvez obtenir un avertissement si vous utilisez un checker de style.

Honnêtement, je serais plus dérangé par le (...).size() que la mission. Cela semble un peu IFFY, parce que vous êtes la déréférence de l'affectation. ;)

Je ne pense pas qu'il y ait une règle difficile, à moins que vous ne suivez strictement les conseils d'un checker de style et cela le signale avec un avertissement.

0
Rob

La duplication est comme la médecine. Il est nocif dans les doses élevées, mais peut être bénéfique lorsqu'il est utilisé de manière appropriée à de faibles doses. Cette situation est l'un des cas utiles, car vous avez déjà refacturé la pire duplication dans la fonction getCurrentStrings(). Je suis d'accord avec Sebastian, cependant, c'est mieux écrit en tant que boucle.

Le long de ces mêmes lignes, si ce modèle est à venir tout le temps, cela pourrait être un signe que vous devez créer de meilleures abstractions ou réorganiser les responsabilités entre différentes classes ou fonctions. Ce qui fait des boucles comme ces problématiques, c'est qu'ils dépendent fortement des effets secondaires. Dans certains domaines qui n'est pas toujours évitable, comme des E/S par exemple, mais vous devriez toujours essayer de pousser les fonctions dépendantes de l'effet secondaire aussi profondément dans vos couches d'abstraction que possible, donc il n'y en a pas beaucoup d'entre eux.

Dans votre code d'exemple, sans connaître le contexte, la première chose que je ferais est d'essayer de trouver un moyen de le refroidir pour faire tout le travail sur ma copie locale de currentStrings, puis mettez à jour l'état externe tout à la fois. . Quelque chose comme:

for(String string: getCurrentStrings()) {
    doSomething(string);
}
clearCurrentStrings();

Si les chaînes actuelles sont mises à jour par un autre thread, un modèle d'événement est souvent en ordre:

void onCurrentStringAdd(String addedString) {
    doSomething(addedString);
}

Vous obtenez la photo. Il y a généralement Certains façon de refroidir votre code pour ne pas dépendre autant d'effets secondaires. Si ce n'est pas pratique, vous pouvez souvent éviter les doubles emplois en déplaçant une certaine responsabilité envers une autre classe, comme dans:

CurrentStrings currentStrings = getCurrentStrings();
while (!currentStrings.empty()) {
    currentStrings.alter();
}

Il peut sembler de surkiller de créer une nouvelle classe CurrentStrings pour quelque chose qui peut être surtout servi par un List<String>, mais cela ouvrira souvent toute une gamme de simplifications tout au long de votre code. Sans parler des avantages de l'encapsulation et de la vérification de type.

0
Karl Bielefeldt