Mon patron ne cesse de mentionner nonchalamment que les mauvais programmeurs utilisent break
et continue
en boucles.
Je les utilise tout le temps parce qu'ils ont du sens; laissez-moi vous montrer l'inspiration:
function verify(object) {
if (object->value < 0) return false;
if (object->value > object->max_value) return false;
if (object->name == "") return false;
...
}
Le point ici est que la fonction vérifie d'abord que les conditions sont correctes, puis exécute la fonctionnalité réelle. L'OMI s'applique également aux boucles:
while (primary_condition) {
if (loop_count > 1000) break;
if (time_exect > 3600) break;
if (this->data == "undefined") continue;
if (this->skip == true) continue;
...
}
Je pense que cela facilite la lecture de & debug; mais je ne vois pas non plus d'inconvénient.
Lorsqu'elles sont utilisées au début d'un bloc, lors des premières vérifications, elles agissent comme des conditions préalables, donc c'est bien.
Lorsqu'ils sont utilisés au milieu du bloc, avec du code autour, ils agissent comme des pièges cachés, donc c'est mauvais.
Vous pouvez lire l'article de Donald Knuth de 1974 Programmation structurée avec go to Statements , dans lequel il discute de diverses utilisations du go to
qui sont structurellement souhaitables. Ils comprennent l'équivalent des instructions break
et continue
(de nombreuses utilisations de go to
y ont été développés en constructions plus limitées). Votre patron est-il du genre à appeler Knuth un mauvais programmeur?
(Les exemples donnés m'intéressent. En général, break
et continue
ne sont pas appréciés par les gens qui aiment une entrée et une sortie de n'importe quel morceau de code, et ce genre de personne fronce également les sourcils sur plusieurs return
instructions.)
Je ne pense pas qu'ils soient mauvais. L'idée qu'ils sont mauvais vient de l'époque de la programmation structurée. Elle est liée à la notion qu'une fonction doit avoir un seul point d'entrée et un seul point de sortie, i. e. un seul return
par fonction.
Cela a du sens si votre fonction est longue et si vous avez plusieurs boucles imbriquées. Cependant, vos fonctions doivent être courtes et vous devez envelopper les boucles et leurs corps dans des fonctions courtes qui leur sont propres. Généralement, forcer une fonction à avoir un seul point de sortie peut entraîner une logique très compliquée.
Si votre fonction est très courte, si vous avez une seule boucle, ou au pire deux boucles imbriquées, et si le corps de la boucle est très court, alors il est très clair ce qu'est un break
ou un continue
Est-ce que. Il est également clair ce que font plusieurs instructions return
.
Ces problèmes sont traités dans "Clean Code" par Robert C. Martin et dans "Refactoring" par Martin Fowler.
Les mauvais programmeurs parlent en absolu (tout comme Sith). Les bons programmeurs utilisent la solution la plus claire possible (toutes choses étant égales par ailleurs).
L'utilisation fréquente de break and continue rend le code difficile à suivre. Mais si les remplacer rend le code encore plus difficile à suivre, c'est un mauvais changement.
L'exemple que vous avez donné est certainement une situation où les pauses et les continuations devraient être remplacées par quelque chose de plus élégant.
La plupart des gens pensent que c'est une mauvaise idée car le comportement n'est pas facilement prévisible. Si vous lisez le code et que vous voyez while(x < 1000){}
vous supposez que cela va fonctionner jusqu'à x> = 1000 ... Mais s'il y a des ruptures au milieu, alors cela ne se vérifie pas, donc vous ne pouvez pas vraiment faire confiance à votre boucle ...
C'est la même raison pour laquelle les gens n'aiment pas GOTO: bien sûr, cela peut être bien utilisé, mais cela peut également conduire à un code de spaghetti divin, où le code saute au hasard d'une section à l'autre.
Pour moi, si je devais faire une boucle qui s'est cassée sur plus d'une condition, je ferais while(x){}
puis basculerais X sur false quand j'aurais besoin de m'éclater. Le résultat final serait le même, et toute personne lisant le code saurait regarder de plus près les choses qui ont changé la valeur de X.
Oui, vous pouvez [ré] écrire des programmes sans instructions break (ou retours depuis le milieu des boucles, qui font la même chose). Mais vous devrez peut-être introduire des variables supplémentaires et/ou une duplication de code qui rendent généralement le programme plus difficile à comprendre. Pascal (le langage de programmation) était très mauvais en particulier pour les programmeurs débutants pour cette raison. Votre patron veut essentiellement que vous programmiez dans les structures de contrôle de Pascal. Si Linus Torvalds était à votre place, il montrerait probablement à votre patron le majeur!
Il y a un résultat informatique appelé la hiérarchie des structures de contrôle de Kosaraju, qui remonte à 1973 et qui est mentionné dans le (plus) célèbre article de Knuth sur les gotos de 1974. (Cet article de Knuth a déjà été recommandé ci-dessus par David Thornley, soit dit en passant) .) Ce que S. Rao Kosaraju a prouvé en 1973, c'est qu'il n'est pas possible de réécrire tous les programmes qui ont des sauts de profondeur à plusieurs niveaux n dans des programmes avec break profondeur inférieure à n sans introduire de variables supplémentaires. Mais disons que ce n'est qu'un résultat purement théorique. (Ajoutez juste quelques variables supplémentaires?! Vous pouvez sûrement le faire pour faire plaisir à votre patron ...)
Ce qui est beaucoup plus important du point de vue de l'ingénierie logicielle est un article plus récent de 1995 d'Eric S. Roberts intitulé Sorties de boucle et programmation structurée: réouverture du débat ( http://cs.stanford.edu/people/eroberts/papers/SIGCSE-1995/LoopExits.pdf ). Roberts résume plusieurs études empiriques menées par d'autres avant lui. Par exemple, lorsqu'un groupe d'étudiants de type CS101 a été invité à écrire du code pour une fonction implémentant une recherche séquentielle dans un tableau, l'auteur de l'étude a déclaré ce qui suit au sujet des étudiants qui ont utilisé une pause/retour/goto pour quitter le la boucle de recherche séquentielle lorsque l'élément a été trouvé:
Je n'ai pas encore trouvé une seule personne qui a tenté un programme en utilisant [ce style] qui a produit une solution incorrecte.
Roberts dit également que:
Les étudiants qui ont tenté de résoudre le problème sans utiliser un retour explicite de la boucle for s'en sont beaucoup moins bien sortis: seuls sept des 42 étudiants qui ont essayé cette stratégie ont réussi à générer des solutions correctes. Ce chiffre représente un taux de réussite inférieur à 20%.
Oui, vous pouvez être plus expérimenté que les étudiants CS101, mais sans utiliser l'instruction break (ou renvoyer/aller de manière équivalente au milieu des boucles), vous finirez par écrire du code qui, bien que nominalement bien structuré, est suffisamment velu en termes de logique supplémentaire les variables et la duplication de code selon lesquelles quelqu'un, probablement vous-même, y mettra des bogues logiques tout en essayant de suivre le style de codage de votre patron.
Je vais également dire ici que le papier de Roberts est beaucoup plus accessible au programmeur moyen, donc une meilleure première lecture que celle de Knuth. Il est également plus court et couvre un sujet plus étroit. Vous pourriez probablement même le recommander à votre patron, même s'il est plutôt gestionnaire que CS.
Je n'envisage pas d'utiliser l'une de ces mauvaises pratiques, mais les utiliser trop dans la même boucle devrait justifier de repenser la logique utilisée dans la boucle. Utilisez-les avec parcimonie.
L'exemple que vous avez donné n'a pas besoin de pause ni de suite:
while (primary-condition AND
loop-count <= 1000 AND
time-exec <= 3600) {
when (data != "undefined" AND
NOT skip)
do-something-useful;
}
Mon "problème" avec les 4 lignes de votre exemple est qu'elles sont toutes au même niveau mais elles font des choses différentes: certaines se cassent, d'autres continuent ... Vous devez lire chaque ligne.
Dans mon approche imbriquée, plus vous approfondissez, plus le code devient "utile".
Mais, si au fond vous trouviez une raison d'arrêter la boucle (autre que la condition primaire), une pause ou un retour aurait son utilité. Je préférerais cela à l'utilisation d'un indicateur supplémentaire qui doit être testé dans la condition de niveau supérieur. La pause/retour est plus directe; il indique mieux l'intention que de définir une autre variable.
La "méchanceté" dépend de la façon dont vous les utilisez. J'utilise généralement les ruptures dans les constructions en boucle UNIQUEMENT lorsque cela me fera économiser des cycles qui ne peuvent pas être enregistrés via une refactorisation d'un algorithme. Par exemple, parcourir une collection à la recherche d'un élément dont la valeur dans une propriété spécifique est définie sur true. Si tout ce que vous devez savoir, c'est que l'un des éléments avait cette propriété définie sur true, une fois que vous obtenez ce résultat, une interruption est bonne pour terminer la boucle de manière appropriée.
Si l'utilisation d'un saut ne rend pas le code spécifiquement plus facile à lire, plus court à exécuter ou enregistre les cycles de traitement de manière significative, alors il est préférable de ne pas les utiliser. J'ai tendance à coder au "plus petit dénominateur commun" lorsque cela est possible pour m'assurer que quiconque me suit peut facilement consulter mon code et comprendre ce qui se passe (je ne réussis pas toujours). Les pauses réduisent cela car elles introduisent des points d'entrée/sortie étranges. Utilisés à mauvais escient, ils peuvent se comporter comme une déclaration "goto" hors de propos.
Absolument pas ... Oui, l'utilisation de goto
est mauvaise car elle détériore la structure de votre programme et il est également très difficile de comprendre le flux de contrôle.
Mais l'utilisation d'instructions comme break
et continue
est absolument nécessaire de nos jours et n'est pas du tout considérée comme une mauvaise pratique de programmation.
Et ce n'est pas si difficile de comprendre le flux de contrôle utilisé par break
et continue
. Dans les constructions comme switch
, l'instruction break
est absolument nécessaire.
Je remplacerais votre deuxième extrait de code par
while (primary_condition && (loop_count <= 1000 && time_exect <= 3600)) {
if (this->data != "undefined" && this->skip != true) {
..
}
}
pas pour des raisons de concision - je pense en fait que c'est plus facile à lire et pour quelqu'un de comprendre ce qui se passe. D'une manière générale, les conditions de vos boucles doivent être contenues uniquement dans ces conditions de boucle non jonchées dans tout le corps. Cependant, dans certaines situations, break
et continue
peuvent améliorer la lisibilité. break
plus que continue
je pourrais ajouter: D
L'idée essentielle vient de pouvoir analyser sémantiquement votre programme. Si vous avez une seule entrée et une seule sortie, les calculs nécessaires pour désigner les états possibles sont considérablement plus faciles que si vous devez gérer les chemins de bifurcation.
En partie, cette difficulté se traduit par la possibilité de raisonner conceptuellement sur votre code.
Franchement, votre deuxième code n'est pas évident. Qu'est-ce que ça fait? Continue-t-elle à "continuer" ou "à côté" de la boucle? Je n'ai aucune idée. Au moins, votre premier exemple est clair.
Je suis d'accord avec ton patron. Ils sont mauvais car ils produisent des méthodes à haute complexité cyclomatique. De telles méthodes sont difficiles à lire et difficiles à tester. Heureusement, il existe une solution simple. Extrayez le corps de la boucle dans une méthode distincte, où "continuer" devient "retour". Le "retour" est meilleur car après le "retour", c'est fini - il n'y a pas de soucis pour l'état local.
Pour "break", extrayez la boucle elle-même dans une méthode distincte, en remplaçant "break" par "return".
Si les méthodes extraites nécessitent un grand nombre d'arguments, c'est une indication pour extraire une classe - soit les collecter dans un objet de contexte.
Je ne suis pas d'accord avec ton patron. Il existe des emplacements appropriés pour break
et continue
à utiliser. En fait, la raison pour laquelle les exceptions et la gestion des exceptions ont été introduites dans les langages de programmation modernes est que vous ne pouvez pas résoudre tous les problèmes en utilisant simplement structured techniques
.
Sur une note secondaire Je ne veux pas commencer une discussion religieuse ici mais vous pourriez restructurer votre code pour être encore plus lisible comme ceci:
while (primary_condition) {
if (loop_count > 1000) || (time_exect > 3600) {
break;
} else if ( ( this->data != "undefined") && ( !this->skip ) ) {
... // where the real work of the loop happens
}
}
Sur une autre note latérale
Personnellement, je n'aime pas l'utilisation de ( flag == true )
dans les conditions car si la variable est déjà un booléen, alors vous introduisez une comparaison supplémentaire qui doit se produire lorsque la valeur du booléen a la réponse que vous voulez - à moins bien sûr que vous ne soyez certain que votre compilateur optimisera cette comparaison supplémentaire un moyen.
Je ne suis pas contre continue
et break
en principe, mais je pense que ce sont des constructions de très bas niveau qui peuvent très souvent être remplacées par quelque chose d'encore mieux.
J'utilise C # comme exemple ici, considérons le cas de vouloir itérer sur une collection, mais nous voulons seulement les éléments qui remplissent un prédicat, et nous ne voulons pas faire plus de 100 itérations au maximum.
for (var i = 0; i < collection.Count; i++)
{
if (!Predicate(item)) continue;
if (i >= 100) break; // at first I used a > here which is a bug. another good thing about the more declarative style!
DoStuff(item);
}
Cela semble RAISONNABLEMENT propre. Ce n'est pas très difficile à comprendre. Je pense que cela gagnerait beaucoup à être plus déclaratif. Comparez-le aux éléments suivants:
foreach (var item in collection.Where(Predicate).Take(100))
DoStuff(item);
Peut-être que les appels Where and Take ne devraient même pas être dans cette méthode. Peut-être que ce filtrage doit être effectué AVANT que la collection soit passée à cette méthode. Quoi qu'il en soit, en s'éloignant des choses de bas niveau et en se concentrant davantage sur la logique métier réelle, il devient plus clair ce qui nous intéresse réellement. Il devient plus facile de séparer notre code en modules cohésifs qui adhèrent davantage aux bonnes pratiques de conception, etc. sur.
Des éléments de bas niveau existeront toujours dans certaines parties du code, mais nous voulons le cacher autant que possible, car il faut plutôt de l'énergie mentale que nous pourrions utiliser pour raisonner sur les problèmes commerciaux.
Je pense que ce n'est un problème que lorsqu'il est imbriqué profondément dans plusieurs boucles. Il est difficile de savoir à quelle boucle vous vous arrêtez. Il peut également être difficile de suivre une poursuite, mais je pense que la vraie douleur vient des pauses - la logique peut être difficile à suivre.
Je n'aime aucun de ces styles. Voici ce que je préférerais:
function verify(object)
{
if not (object->value < 0)
and not(object->value > object->max_value)
and not(object->name == "")
{
do somethign important
}
else return false; //probably not necessary since this function doesn't even seem to be defined to return anything...?
}
Je n'aime vraiment pas utiliser return
pour abandonner une fonction. Cela ressemble à un abus de return
.
L'utilisation de break
n'est pas toujours claire à lire.
Mieux encore, cela pourrait être:
notdone := primarycondition
while (notDone)
{
if (loop_count > 1000) or (time_exect > 3600)
{
notDone := false;
}
else
{
skipCurrentIteration := (this->data == "undefined") or (this->skip == true)
if not skipCurrentIteration
{
do something
}
...
}
}
moins d'imbrication et les conditions complexes sont refactorisées en variables (dans un vrai programme, il faudrait avoir de meilleurs noms, évidemment ...)
(Tout le code ci-dessus est un pseudo-code)
Non. C'est un moyen de résoudre un problème, et il existe d'autres moyens de le résoudre.
De nombreux langages courants (Java, .NET (C # + VB), PHP, écrivez le vôtre) utilisent "break" et "continue" pour ignorer les boucles. Ils ont tous les deux des phrases "goto (s) structurées".
Sans eux:
String myKey = "mars";
int i = 0; bool found = false;
while ((i < MyList.Count) && (not found)) {
found = (MyList[i].key == myKey);
i++;
}
if (found)
ShowMessage("Key is " + i.toString());
else
ShowMessage("Not found.");
Avec eux:
String myKey = "mars";
for (i = 0; i < MyList.Count; i++) {
if (MyList[i].key == myKey)
break;
}
ShowMessage("Key is " + i.toString());
Notez que le code "break" et "continue" est plus court et transforme généralement les phrases "while" en "for" ou "foreach".
Les deux cas sont une question de style de codage. je préfère ne pas les utiliser, car le style verbeux me permet d'avoir plus de contrôle sur le code.
En fait, je travaille dans certains projets, où il était obligatoire d'utiliser ces phrases.
Certains développeurs peuvent penser qu'ils ne sont pas nécéssairement, mais hypothétiques, si nous devions les supprimer, nous devons supprimer "tandis" et "faire pendant" ("répéter jusqu'à", vous les gars Pascal) aussi ;-)
Conclusion, même si je préfère ne pas les utiliser, je pense que c'est une option, pas une mauvaise pratique de programmation.
Tant qu'ils ne sont pas utilisés comme goto déguisés comme dans l'exemple suivant:
do
{
if (foo)
{
/*** code ***/
break;
}
if (bar)
{
/*** code ***/
break;
}
} while (0);
Je vais bien avec eux. (Exemple vu dans le code de production, meh)