web-dev-qa-db-fra.com

Clarification des conseils "éviter si autre chose"

Les experts en code propre conseillent de ne pas utiliser if/else car il crée un code illisible. Ils suggèrent plutôt d'utiliser IF et de ne pas attendre la fin d'une méthode sans réel besoin.

Maintenant ça if/else les conseils me confondent. Disent-ils que je ne devrais pas utiliser if/elsepas du tout (!) ou pour éviter if/else imbrication?

De plus, s'ils font référence à l'imbrication de if/else, ne devrais-je pas faire une seule imbrication ou la limiter à 2 imbrications maximum (comme certains le recommandent)?

Quand je dis l'emboîtement unique, je veux dire ceci:

    if (...) {

        if (...) {

        } else {

        }

    } else {

    }

MODIFIER

Des outils comme Resharper suggéreront également de reformater if/else déclarations. Il les concède généralement à if déclaration autonome, et parfois même à l'expression ternaire.

39
deviDave

Je pense que ce conseil vient d'une métrique logicielle qui appelait Complexité cyclomatique ou Complexité conditionnelle check cette page wiki

Définition:

La complexité cyclomatique d'une section de code source est le décompte du nombre de chemins linéairement indépendants à travers le code source. Par exemple, si le code source ne contenait aucun point de décision tel que des instructions IF ou des boucles FOR, la complexité serait de 1, car il n'y a qu'un seul chemin à travers le code. Si le code avait une seule instruction IF contenant une seule condition, il y aurait deux chemins à travers le code, un chemin où l'instruction IF est évaluée comme TRUE et un chemin où l'instruction IF est évaluée comme FALSE.

Donc

Disent-ils que je ne devrais pas utiliser if/else du tout (!) Ou pour éviter l'imbrication if/else?

Non, vous ne devriez pas éviter du tout! mais vous devez faire attention à la complexité cyclomatique de votre code. Plus la complexité du code a tendance à avoir plus de défauts. Selon this la bonne limite serait 11 . Un outil appelé Code complet catégorise le score comme ça Réf. :

  • 0-5 - la routine est probablement bien
  • 6-10 - commencez à réfléchir aux moyens de simplifier la routine
  • 10 + - divise une partie de la routine en une deuxième routine et l'appelle depuis la première routine

Comment calculer "fondamentalement" la complexité cyclomatique:

De l'exemple ici : considérez le pseudo code suivant:

if (A=354) {

    if (B>C) {/*decision 1*/ A=B} else {/*decision 2*/ A=C}

   }/*decision 3 , in case A is NOT equal 354*/ 
print A

veuillez noter que cette métrique est préoccupée par le nombre de décisions dans votre programme/code, donc à partir du code ci-dessus, nous pourrions dire que nous avons 3 décisions (3 valeurs de A)

calculons-le formellement, alors considérons le flux de contrôle suivant du code: enter image description here

La complexité M est alors définie * comme: Ref

M = E − N + 2P

E = the number of edges of the graph
N = the number of nodes of the graph
P = the number of connected components (exit nodes).

en appliquant la formule E(flow lines) = 8, N (nodes)= 7, P (exit nodes) =1 puis M = 8-7 + (2*1) = 3

* il y a aussi une autre définition mais elle est proche: M = E − N + P vérifiez la référence pour la théorie.

Veuillez noter que de nombreux outils d'analyse de code statique pourraient calculer la complexité cyclomatique de votre code.

43
Abdurahman

OMI, les réponses précédentes se concentrent sur les cas et le code Edge qui devraient être réécrits de toute façon. Voici quelques cas que je vois presque partout dans lesquels if/else aurait dû être remplacé par if ou une autre construction de langage.

Remarque: je crée le wiki de la communauté des réponses. N'hésitez pas à le modifier et à ajouter d'autres exemples de cas où if/else conduit généralement à un mauvais code.

Débutants, soyez préven ! if/else a le potentiel de créer un code illisible lorsqu'il n'est pas utilisé avec précaution. Les exemples suivants montrent les cas les plus courants où if/else est facilement détourné.

1. Clause de garde

J'ai arrêté de compter combien de fois j'ai vu quelque chose comme ça:

void Demo(...)
{
    if (ok_to_continue)
    {
        ... // 20 lines here.
    }
    else
    {
        throw new SomeException();
    }
}

Personne n'a besoin d'une indentation supplémentaire. Le même code aurait pu être écrit comme ceci:

void Demo(...)
{
    if (!ok_to_continue)
    {
        throw new SomeException();
    }

    ... // No indentation for the next 20 lines.
}

Règle: si vous pouvez supprimer une instruction else en inversant la vérification if, en gérant une erreur et en quittant immédiatement, (sinon sinon continuez), alors faites-le.

2. Duplication de code

Celui-ci est également assez fréquent.

if (!this.useAlternatePanel)
{
    currentInput = this.defaultTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.defaultProcessButton.Enabled = !missingInput;
    this.defaultMissingInputHelpLabel.Visible = missingInput;
}
else
{
    currentInput = this.alternateTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.alternateProcessButton.Enabled = !missingInput;
    this.alternateMissingInputHelpLabel.Visible = missingInput;
}

Ceci est un excellent exemple de code pour débutants, car un débutant ne verrait pas toujours comment un tel code peut être refactorisé. L'une des façons consiste à créer une carte de contrôles:

var defaultControls = new CountriesProcessingControls(
    this.defaultTextBox,
    this.defaultProcessButton,
    this.defaultMissingInputHelpLabel);

var alternateControls = new CountriesProcessingControls(
    this.alternateTextBox,
    this.alternateProcessButton,
    this.alternateMissingInputHelpLabel);

void RefreshCountriesControls(CountriesProcessingControls controls)
{
    currentInput = controls.textBox.Text;
    bool missingInput = currentInput.Length == 0;
    controls.processButton.Enabled = !missingInput;
    controls.missingInputHelpLabel.Visible = missingInput;
}

RefreshCountriesControls(this.useAlternatePanel ? alternateControls : defaultControls);

Règle: si le bloc dans else est presque identique au bloc dans if, vous le faites mal et provoquez la duplication de code.

3. Essayer de faire deux choses

L'inverse est également courant:

if (...)
{
    foreach (var dateInput = this.selectedDates)
    {
        var isConvertible = convertToDate(dateInput);
        if (!isConvertible)
        {
            this.invalidDates.add(dateInput);
        }
    }
}
else
{
    var languageService = this.initializeLanguageService();
    this.Translated = languageService.translate(this.LastInput);
    if (this.Translated != null)
    {
        this.NotificationScheduler.append(LAST_INPUT_TRANSLATED);
    }
}

Le bloc dans if n'a rien à voir avec le bloc dans else. La combinaison de deux blocs dans la même méthode rend la logique difficile à comprendre et sujette aux erreurs. Il est également extrêmement difficile de trouver un nom pour la méthode et de la commenter correctement.

Règle: évitez de combiner des blocs qui n'ont rien en commun dans un if/else bloquer.

4. Programmation orientée objet

Ce type de codage est également fréquent chez les débutants:

// `animal` is either a dog or a cat.
if (animal.IsDog)
{
    animal.EatPedigree();
    animal.Bark();
}
else // The animal is a cat.
{
    animal.EatWhiskas();
    animal.Meow();
}

En programmation orientée objet, ce code ne devrait pas exister. Il devrait être écrit comme ceci à la place:

IAnimal animal = ...;
animal.Eat();
animal.MakeSound();

étant donné que chaque classe (Cat : IAnimal et Dog : IAnimal) aura leur propre implémentation de ces méthodes.

Règle: lors de la programmation orientée objet, généralisez les fonctions (méthodes) et utilisez l'héritage et le polymorphisme.

5. Attribuer une valeur

Je vois celui-ci partout dans des bases de code écrites par des débutants:

int price;
if (this.isDiscount)
{
    price = -discount;
}
else
{
    price = unitCost * quantity;
}

C'est un morceau de code terrible, car il n'est pas écrit pour les humains.

  • C'est trompeur. Cela vous fait penser que l'opération principale ici est le choix entre le mode remise et non remise, tandis que l'opération principale réelle est l'affectation de price.

  • Il est sujet aux erreurs. Et si plus tard, une condition est modifiée? Que faire si l'une des deux affectations est supprimée?

  • Il invite un code encore pire. J'ai vu beaucoup de morceaux comme ça où int price était à la place int price = 0;, juste pour se débarrasser de l'avertissement du compilateur si l'une des affectations est manquante. Cacher les avertissements au lieu de les résoudre est l'une des pires choses que l'on puisse faire.

  • Il y a une petite duplication de code, ce qui signifie plus de caractères à taper à l'origine et plus de changements à faire plus tard.

  • C'est inutilement long.

La même chose pourrait être écrite comme ceci:

int price = this.isDiscount ? -discount : unitCost * quantity;

Règle: utilisez l'opérateur ternaire au lieu de if/else blocs pour les affectations de valeurs simples.

63
Arseni Mourzenko

J'aime vraiment ce que Abdurahman a dit dans sa réponse.

Une autre façon d'examiner l'énoncé "éviter si/sinon" est de penser en termes de décisions.

L'objectif est d'éviter toute confusion lors du suivi du code. Lorsque plusieurs instructions if/else sont imbriquées, il devient difficile de comprendre l'objectif principal des fonctions. Le programmeur qui écrit le code comprend la logique maintenant. MAIS, lorsque ce même programmeur revisite ce code un an ou deux plus tard, la compréhension est généralement oubliée. Un nouveau programmeur qui regarde le code doit être capable d'identifier ce qui se passe. L'objectif est donc de décomposer la complexité en fonctions simples et faciles à comprendre. L'objectif n'est pas d'éviter les décisions, mais d'établir un flux de logique facile à comprendre.

8
Paul Grimes

L'imbrication de blocs de code, également appelée anti-modèle de "pointe de flèche", réduit la lisibilité du code car vous devez soit continuer à décaler vers la droite, en réduisant la largeur disponible pour les lignes de code "réel" à l'écran, soit violer l'indentation et autres des règles de formatage qui, dans le cas normal, facilitent la lecture du code.

Exemple concret:

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   DoDefault();
}

Ceci est un exemple simple et je n'aurais aucun problème avec quelques niveaux de ce type d'imbrication, fourni que le LOC total pour cette structure n'était que d'un écran de long, et que la mise en forme impliquée ne N'ajoutez pas trop d'espace sur le côté gauche (pensez à utiliser 3 ou 4 espaces au lieu d'un espace de tabulation, surtout si le code sera affiché dans d'autres éditeurs où un espace de tabulation peut être de 8 espaces ou plus). Au-delà de cela, vous pouvez oublier ou difficilement voir la condition initiale dans laquelle les autres sont imbriqués. Cela peut vous faire "perdre" dans votre base de code, surtout si vous avez une structure où X et Y sont indépendants:

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   if (Y) {
       DoYOnly();
    } else {
       DoDefault();
    }
}

Dans cet univers alternatif à la question spécifique de l'OP, nous avons maintenant deux vérifications de condition "si (Y)", et si, dans un vrai morceau de code, nous ne pouvions pas voir la condition parent imbriquée, nous pourrions penser que nous sommes dans un endroit différent du code que nous ne le sommes réellement, ce qui provoque des bugs.

Une façon de réduire l'emboîtement consiste à répartir la condition extérieure dans les conditions intérieures. De retour à votre extrait initial (pas de "DoYOnly ()"), tenez compte des éléments suivants:

if (X && Y) {
    DoXandY();
} else if (X) {
    DoXOnly();
} else {
    DoDefault();
}

Regardez Ma, pas de nidification! Au prix d'une évaluation supplémentaire de X (qui, s'il est complexe sur le plan du calcul d'évaluer X, vous pouvez effectuer une opération initiale et la stocker dans une variable booléenne), vous faites de la structure un arbre de décision if/else à un niveau, et quand en éditant chaque branche, vous connaissez la condition qui doit être vraie pour y entrer. Vous devez toujours vous rappeler ce que n'était pas vrai pour arriver à ce "sinon si", mais parce que tout est au même niveau, il devrait être plus facile d'identifier les branches précédentes de l'arbre de décision. Alternativement, vous pouvez supprimer tous les autres mots clés en testant une version plus complète de la condition à chaque étape:

if (X && Y) {
    DoXandY();
} 
if (X && !Y) {
    DoXOnly();
} 
if(!X) {
    DoDefault();
}

Vous savez maintenant exactement ce qui doit être vrai pour chaque instruction à exécuter. Cependant, cela commence à faire face à une série d'autres problèmes de lisibilité; il devient plus difficile de voir que toutes ces déclarations s'excluent mutuellement (ce qui fait partie de ce qu'un bloc "else" fait pour vous).

D'autres façons d'inclure l'imbrication incluent l'inversion d'une ou plusieurs conditions. Si la plupart ou la totalité du code d'une fonction se trouve dans une instruction if testant une condition, vous pouvez généralement vous enregistrer ce niveau d'imbrication en testant plutôt que la condition est pas vraie, et en effectuant tout ce que vous 'ferait dans un bloc else au-delà de l'instruction if d'origine, avant de quitter la fonction. Par exemple:

if(!X) {
    DoDefault();
    return;
}

if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

Cela se comporte de la même manière que les deux instructions ci-dessus, mais ne teste chaque condition qu'une seule fois et n'a pas d'if imbriqués. Ce modèle est parfois appelé "clause de garde"; le code sous l'instruction if initiale ne s'exécutera jamais si X est faux, vous pouvez donc dire que l'instruction if "garde" le reste du code (c'est pourquoi nous n'avons jamais besoin de tester que X est vrai dans le reste du code; si nous arrivons à cette ligne, X est vrai car nous aurions quitté la fonction tôt sinon). L'inconvénient est que cela ne fonctionne pas aussi bien pour le code qui est une plus petite partie d'une plus grande fonction. S'il y avait une fonction "DoMoreWork ()" après l'intégralité de l'extrait ci-dessus, elle devrait être incluse à la fois dans le code de la clause de garde et en dessous de l'instruction conditionnelle restante. Ou, la clause guard pourrait être intégrée au code restant en utilisant le modèle "else if":

if(!X) {
    DoDefault();        
} else if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

DoMoreWork();

Dans cet exemple, DoMoreWork () est toujours exécuté même si X est faux. L'ajout du même appel sous l'extrait précédent avec l'instruction return entraînerait l'appel de DoMoreWork uniquement si X est vrai, mais que Y soit vrai ou faux.

7
KeithS

En plus des autres réponses, les conseils peuvent également signifier que if/else Devrait être remplacé par if/else if Le cas échéant. Ce dernier est plus informatif et lisible (à l'exclusion bien sûr des cas triviaux, comme if (x > 0) else if (x <= 0)).

if/else if A un avantage de plus que d'être plus lisible (bien que ce soit aussi un avantage considérable). Cela aide le programmeur à éviter les erreurs au cas où il y aurait plus de deux branches selon la condition. Un tel style rendra le cas missed plus évident.

1
superM

La raison pour laquelle ils vous conseillent d'utiliser uniquement les instructions if et non else est d'éviter le bogue classique de if this then for everything else do this. La portée de else est difficile à prévoir.

Par exemple; disons que je programme les commandes d'une voiture autonome.

if (light is red) then {
    stop car
} else {
    drive forward
}

Alors que la logique est solide. Lorsque la voiture arrive à un feu jaune, elle continue à traverser l'intersection. Probablement à l'origine d'un accident alors qu'il aurait dû ralentir et s'arrêter.

Sans le if/else la logique est moins sujette aux erreurs.

if(light is red) then {
  stop car
}
if(light is yellow) then {
  slow down
}
if(light is green) then {
  drive forward
}

Bien que mon code ne soit pas la meilleure voiture autonome. Il montre simplement la différence entre une condition ambiguë et des conditions explicites. C'est ce que l'auteur faisait probablement référence également. N'utilisez pas else pour raccourcir la logique.

1
Reactgular