tandis que (vrai) et rupture de boucle - anti-modèle?


32

Considérez le code suivant:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Supposons que ce processus implique un nombre d'étapes fini mais dépendant de l'entrée; la boucle est conçue pour se terminer d'elle-même à la suite de l'algorithme et n'est pas conçue pour s'exécuter indéfiniment (jusqu'à ce qu'elle soit annulée par un événement extérieur). Parce que le test pour voir si la boucle doit se terminer se trouve au milieu d'un ensemble logique d'étapes, la boucle while elle-même ne vérifie actuellement rien de significatif; la vérification est effectuée à la place «appropriée» dans l'algorithme conceptuel.

On m'a dit que c'était du mauvais code, car il était plus sujet aux bogues car la condition de fin n'était pas vérifiée par la structure de la boucle. Il est plus difficile de comprendre comment vous sortiriez de la boucle, et pourrait inviter des bogues car la condition de rupture pourrait être contournée ou omise accidentellement en raison de changements futurs.

Maintenant, le code pourrait être structuré comme suit:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

Cependant, cela duplique un appel à une méthode dans le code, violant DRY; s'ils TransformInSomeWayétaient plus tard remplacés par une autre méthode, les deux appels devraient être trouvés et modifiés (et le fait qu'il y en ait deux soit moins évident dans un morceau de code plus complexe).

Vous pouvez également l'écrire comme:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... mais vous avez maintenant une variable dont le seul but est de déplacer la vérification de condition vers la structure de la boucle, et doit également être vérifiée plusieurs fois pour fournir le même comportement que la logique d'origine.

Pour ma part, je dis qu'étant donné l'algorithme que ce code implémente dans le monde réel, le code original est le plus lisible. Si vous le traversiez vous-même, c'est ainsi que vous le penseriez, et il serait donc intuitif pour les personnes familiarisées avec l'algorithme.

Alors, qui est "mieux"? vaut-il mieux confier la responsabilité de la vérification de l'état à la boucle while en structurant la logique autour de la boucle? Ou est-il préférable de structurer la logique d'une manière "naturelle" comme indiqué par les exigences ou une description conceptuelle de l'algorithme, même si cela peut signifier contourner les capacités intégrées de la boucle?


12
Peut-être, mais malgré les échantillons de code, la question posée est OMI plus conceptuelle, ce qui correspond davantage à ce domaine de SE. Ce qui est un modèle ou un anti-modèle est souvent discuté ici, et c'est la vraie question; structurer une boucle pour qu'elle s'exécute à l'infini puis en sortir est-il une mauvaise chose?
KeithS

3
La do ... untilconstruction est également utile pour ces types de boucles car elles n'ont pas besoin d'être "amorcées".
Blrfl

2
Le cas de la boucle et demie n'a toujours pas vraiment de bonne réponse.
Loren Pechtel

1
"Cependant, cela reproduit un appel à une méthode dans le code, violant DRY" Je ne suis pas d'accord avec cette affirmation et cela semble être une incompréhension du principe.
Andy

1
En dehors de moi préférant le style C pour (;;) qui signifie explicitement "J'ai une boucle, et je ne vérifie pas l'instruction de boucle", votre première approche est tout à fait juste. Vous avez une boucle. Il y a du travail à faire avant de pouvoir décider de quitter. Ensuite, vous quittez si certaines conditions sont remplies. Ensuite, vous effectuez le travail réel et recommencez tout. C'est tout à fait correct, facile à comprendre, logique, non redondant et facile à comprendre. La deuxième variante est tout simplement horrible, tandis que la troisième est simplement inutile; tourner à terrible si le reste de la boucle qui va dans le si est très long.
gnasher729

Réponses:


14

Restez avec votre première solution. Les boucles peuvent devenir très impliquées et une ou plusieurs coupures nettes peuvent être beaucoup plus faciles pour vous, quiconque regarde votre code, l'optimiseur et les performances du programme que d'élaborer des instructions if et des variables ajoutées. Mon approche habituelle est de mettre en place une boucle avec quelque chose comme "while (true)" et d'obtenir le meilleur codage possible. Souvent, je peux et je remplace alors les ruptures par un contrôle de boucle standard; la plupart des boucles sont assez simples, après tout. La condition de fin doit être vérifiée par la structure de la boucle pour les raisons que vous mentionnez, mais pas au prix de l'ajout de nouvelles variables entières, de l'ajout d'instructions if et de la répétition du code, qui sont tous encore plus sujets aux bogues.


"if-instructions, et la répétition du code, qui sont tous encore plus sujets aux bogues." - Ouais quand j'essaye des gens que j'appelle si c'est des "futurs bugs"
Pat

13

Ce n'est un problème significatif que si le corps de la boucle n'est pas trivial. Si le corps est si petit, alors l'analyser comme ça est trivial, et pas un problème du tout.


7

J'ai du mal à trouver les autres solutions mieux que celle avec break. Bon, je préfère la voie Ada qui permet de faire

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

mais la solution de rupture est le rendu le plus propre.

Mon POV est que lorsqu'il manque une structure de contrôle, la solution la plus propre est de l'imiter avec d'autres structures de contrôle, sans utiliser de flux de données. (Et de même, lorsque j'ai un problème de flux de données, je préfère des solutions de flux de données pures à celle impliquant des structures de contrôle *).

Il est plus difficile de savoir comment sortir de la boucle,

Ne vous méprenez pas, la discussion n'aurait pas lieu si la difficulté n'était pas la même: la condition est la même et présente au même endroit.

Lequel de

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

et

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

rendre mieux la structure prévue? Je vote pour le premier, vous n'avez pas à vérifier que les conditions correspondent. (Mais voyez mon indentation).


1
Oh, regardez, un langage qui supporte directement les boucles de mi-sortie! :)
mjfgates

J'aime votre point sur l'émulation de structures de contrôle manquantes avec des structures de contrôle. C'est probablement aussi une bonne réponse aux questions liées à GOTO. Il faut utiliser les structures de contrôle d'un langage chaque fois qu'elles correspondent aux exigences sémantiques, mais si un algorithme nécessite autre chose, il faut utiliser la structure de contrôle requise par l'algorithme.
supercat

3

tandis que (vrai) et break est un idiome commun. C'est la manière canonique d'implémenter des boucles de mi-sortie dans des langages de type C. Ajouter une variable pour stocker la condition de sortie et un if () autour de la seconde moitié de la boucle est une alternative raisonnable. Certains magasins peuvent uniformiser officiellement l'un ou l'autre, mais aucun n'est supérieur; ils sont équivalents.

Un bon programmeur travaillant dans ces langages devrait être capable de savoir ce qui se passe en un coup d'œil s'il voit l'un ou l'autre. Cela pourrait faire une bonne petite question d'entrevue; remettez à quelqu'un deux boucles, une en utilisant chaque idiome, et demandez-leur quelle est la différence (bonne réponse: "rien", avec peut-être un ajout de "mais j'utilise habituellement celui-là.")


2

Vos alternatives ne sont pas aussi mauvaises que vous ne le pensez. Un bon code devrait:

  1. Indiquez clairement avec de bons noms de variables / commentaires dans quelles conditions la boucle doit être terminée. (La condition de rupture ne doit pas être cachée)

  2. Indiquez clairement l'ordre des opérations. C'est là que placer la 3e opération optionnelle ( DoSomethingElseTo(input)) à l'intérieur du if est une bonne idée.

Cela dit, il est facile de laisser les instincts perfectionnistes et de polissage du laiton prendre beaucoup de temps. Je voudrais juste modifier le si comme mentionné ci-dessus; commentez le code et continuez.


Je pense que les instincts perfectionnistes de polissage du laiton font gagner du temps à long terme. Espérons qu'avec la pratique, vous développez de telles habitudes pour que votre code soit parfait et son laiton poli sans consommer beaucoup de temps. Mais contrairement à la construction physique, les logiciels peuvent durer éternellement et être utilisés dans un nombre infini de lieux. Les économies réalisées grâce à un code qui est même 0,00000001% plus rapide, plus fiable, utilisable et maintenable peuvent être importantes. (Bien sûr, la plupart de mon code très soigné est dans des langues obsolètes depuis longtemps ou fait de l'argent pour quelqu'un d'autre de nos jours, mais cela m'a aidé à développer de bonnes habitudes.)
RalphChapin

Eh bien , je ne peux pas vous tromper :-) Ce appeler est une question d'opinion et si de bonnes compétences sont sortis du laiton polissage: grande.
Pat

Je pense que c'est une possibilité à considérer, de toute façon. Je ne voudrais certainement pas offrir de garanties. Cela me fait me sentir mieux de penser que mon polissage du laiton a amélioré mes compétences, donc je peux avoir des préjugés sur ce point.
RalphChapin

2

Les gens disent que c'est une mauvaise pratique à utiliser while (true), et la plupart du temps, ils ont raison. Si votre whileinstruction contient beaucoup de code compliqué et qu'il n'est pas clair quand vous sortez de if, alors vous vous retrouvez avec du code difficile à maintenir et un simple bug pourrait provoquer une boucle infinie.

Dans votre exemple, vous avez raison d'utiliser while(true)car votre code est clair et facile à comprendre. Il est inutile de créer du code inefficace et plus difficile à lire, simplement parce que certaines personnes considèrent que c'est toujours une mauvaise pratique à utiliser while(true).

J'étais confronté à un problème similaire:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

L'utilisation do ... while(true)évite d' isset($array][$key]être appelée deux fois inutilement, le code est plus court, plus propre et plus facile à lire. Il n'y a absolument aucun risque qu'un bug de boucle infinie soit introduit else break;à la fin.

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Il est bon de suivre les bonnes pratiques et d'éviter les mauvaises pratiques, mais il y a toujours des exceptions et il est important de garder l'esprit ouvert et de réaliser ces exceptions.


0

Si un flux de contrôle particulier s'exprime naturellement sous la forme d'une instruction break, il n'est pratiquement jamais préférable d'utiliser d'autres mécanismes de contrôle de flux pour émuler une instruction break.

(notez que cela inclut le déroulement partiel de la boucle et le glissement du corps de la boucle vers le bas de sorte que la boucle commence coïncide avec le test conditionnel)

Si vous pouvez réorganiser votre boucle afin que le flux de contrôle s'exprime naturellement en ayant une vérification conditionnelle au début de la boucle, tant mieux. Cela vaut peut-être même la peine de réfléchir à la possibilité. Mais non, n'essayez pas de placer une cheville carrée dans un trou rond.


0

Vous pouvez également aller avec ceci:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Ou moins confus:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}

1
Bien que certaines personnes l'aiment, j'affirme que la condition de boucle devrait généralement être réservée aux tests conditionnels, plutôt qu'aux instructions qui font des choses.

5
Expression virgule en boucle ... C'est affreux. Tu es trop intelligent. Et cela ne fonctionne que dans ce cas trivial où TransformInSomeWay peut être exprimé comme une expression.
gnasher729

0

Lorsque la complexité de la logique devient difficile à utiliser, l'utilisation d'une boucle sans limite avec des interruptions en milieu de boucle pour le contrôle de flux est en fait la plus logique. J'ai un projet avec du code qui ressemble à ceci. (Remarquez l'exception à la fin de la boucle.)

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Pour faire cette logique sans rupture de boucle illimitée, je me retrouverais avec quelque chose comme ceci:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Ainsi, l'exception est maintenant levée dans une méthode d'assistance. De plus, il a beaucoup d' if ... elseimbrication. Je ne suis pas satisfait de cette approche. Par conséquent, je pense que le premier modèle est le meilleur.


En effet, j'ai posé cette question sur SO et j'ai été abattu dans les flammes ... stackoverflow.com/questions/47253486/…
intrepidis
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.