web-dev-qa-db-fra.com

Évaluation des courts-circuits, est-ce une mauvaise pratique?

Quelque chose que je connais depuis longtemps mais que je n'ai jamais considéré, c'est que dans la plupart des langues, il est possible de donner la priorité aux opérateurs dans une instruction if en fonction de leur ordre. J'utilise souvent cela comme un moyen d'empêcher les exceptions de référence nulles, par exemple:

if (smartphone != null && smartphone.GetSignal() > 50)
{
   // Do stuff
}

Dans ce cas, le code entraînera d'abord la vérification que l'objet n'est pas nul, puis l'utilisation de cet objet en sachant qu'il existe. Le langage est intelligent car il sait que si la première instruction est fausse, alors il n'y a même aucun intérêt à évaluer la deuxième instruction et donc une exception de référence nulle n'est jamais levée. Cela fonctionne de la même manière pour les opérateurs and et or.

Cela peut également être utile dans d'autres situations, telles que la vérification qu'un index est dans les limites d'un tableau et qu'une telle technique peut être effectuée dans différents langages, à ma connaissance: Java, C #, C++, Python et Matlab.

Ma question est: Ce type de code est-il une représentation d'une mauvaise pratique? Cette mauvaise pratique découle-t-elle d'un problème technique caché (c'est-à-dire que cela pourrait éventuellement entraîner une erreur) ou conduit-il à un problème de lisibilité pour d'autres programmeurs? Cela pourrait-il être déroutant?

89
innova

Non, ce n'est pas une mauvaise pratique. S'appuyer sur le court-circuit des conditions est une technique largement acceptée et utile - tant que vous utilisez n langage qui garantit ce comportement (qui inclut la grande majorité des langues modernes).

Votre exemple de code est assez clair et, en effet, c'est souvent la meilleure façon de l'écrire. Des alternatives (telles que les instructions imbriquées if) seraient plus compliquées, plus compliquées et donc plus difficiles à comprendre.

Quelques mises en garde:

Faites preuve de bon sens concernant la complexité et la lisibilité

Ce qui suit n'est pas une si bonne idée:

if ((text_string != null && replace(text_string,"$")) || (text_string = get_new_string()) != null)

Comme de nombreuses façons "intelligentes" de réduire les lignes de votre code, une dépendance excessive à cette technique peut entraîner un code difficile à comprendre et sujet aux erreurs.

Si vous devez gérer des erreurs, il existe souvent une meilleure solution

Votre exemple est parfait tant qu'il s'agit d'un état de programme normal pour que le smartphone soit nul. Si, cependant, il s'agit d'une erreur, vous devez intégrer une gestion des erreurs plus intelligente.

Évitez les vérifications répétées de la même condition

Comme le souligne Neil, de nombreuses vérifications répétées de la même variable dans différentes conditions sont la preuve la plus probable d'un défaut de conception. Si vous avez dix instructions différentes dispersées dans votre code qui ne devraient pas être exécutées si smartphone est null, demandez-vous s'il existe une meilleure façon de gérer cela que de vérifier la variable à chaque fois.

Ce n'est pas vraiment une question de court-circuit; c'est un problème plus général de code répétitif. Cependant, il convient de le mentionner, car il est assez fréquent de voir du code contenant de nombreuses instructions répétées comme votre exemple d'instruction.

125
user82096

Supposons que vous utilisiez un langage de style C sans && Et que vous deviez faire le code équivalent comme dans votre question.

Votre code serait:

if(smartphone != null)
{
  if(smartphone.GetSignal() > 50)
  {
    // Do stuff
  }
}

Ce modèle apparaîtrait souvent.

Imaginez maintenant que la version 2.0 de notre langage hypothétique présente &&. Pensez à quel point vous pensez que c'était cool!

&& Est le moyen idiomatique reconnu de faire ce que fait mon exemple ci-dessus. Ce n'est pas une mauvaise pratique de l'utiliser, en effet c'est une mauvaise pratique de ne pas l'utiliser dans des cas comme ceux ci-dessus: Un codeur expérimenté dans un tel langage se demanderait où était le else ou une autre raison de ne pas faire les choses dans le mode normal.

Le langage est intelligent car il sait que si la première instruction est fausse, alors il n'y a même aucun intérêt à évaluer la deuxième instruction et donc une exception de référence nulle n'est jamais levée.

Non, vous êtes intelligent, car vous saviez que si la première déclaration était fausse, il n'y a même aucun intérêt à évaluer la deuxième déclaration. La langue est stupide comme une boîte de rochers et a fait ce que vous lui aviez dit de faire. (John McCarthy était encore plus intelligent et a réalisé que l'évaluation des courts-circuits serait une chose utile pour les langues).

La différence entre être intelligent et la langue intelligente est importante, car les bonnes et les mauvaises pratiques se résument souvent à être aussi intelligent que nécessaire, mais pas plus intelligent.

Considérer:

if(smartphone != null && smartphone.GetSignal() > ((range = (range != null ? range : GetRange()) != null && range.Valid ? range.MinSignal : 50)

Cela étend votre code pour vérifier un range. Si range est null, il essaie de le définir par un appel à GetRange() bien que cela puisse échouer, donc range peut toujours être null. Si la plage n'est pas nulle après cela et qu'elle est Valid alors sa propriété MinSignal est utilisée, sinon une valeur par défaut de 50 Est utilisée.

Cela dépend aussi de &&, Mais le mettre sur une seule ligne est probablement trop intelligent (je ne suis pas sûr à 100% que j'ai raison, et je ne vais pas revérifier parce que ce fait démontre mon point de vue ).

Ce n'est pas && Que c'est le problème ici, mais la capacité de l'utiliser pour mettre beaucoup dans une expression (une bonne chose) augmente notre capacité à écrire inutilement des expressions difficiles à comprendre (une mauvaise chose) .

Aussi:

if(smartphone != null && (smartphone.GetSignal() == 2 || smartphone.GetSignal() == 5 || smartphone.GetSignal() == 8 || smartPhone.GetSignal() == 34))
{
  // do something
}

Ici, je combine l'utilisation de && Avec une vérification de certaines valeurs. Ce n'est pas réaliste dans le cas des signaux téléphoniques, mais cela se produit dans d'autres cas. Ici, c'est un exemple de mon manque d'intelligence. Si j'avais fait ce qui suit:

if(smartphone != null)
{
  switch (smartphone.GetSignal())
  {
    case 2: case 5: case 8: case 34:
      // Do something
      break;
  }
}

J'aurais gagné en lisibilité et en performances (les multiples appels à GetSignal() n'auraient probablement pas été optimisés).

Là encore, le problème n'est pas tant && Que de prendre ce marteau particulier et de voir tout le reste comme un clou; ne pas l'utiliser me permet de faire quelque chose de mieux que de l'utiliser.

Un dernier cas qui s'éloigne des meilleures pratiques est le suivant:

if(a && b)
{
  //do something
}

Par rapport à:

if(a & b)
{
  //do something
}

L'argument classique pour expliquer pourquoi nous pourrions favoriser ce dernier est qu'il y a un certain effet secondaire dans l'évaluation de b que nous voulons que a soit vrai ou non. Je ne suis pas d'accord sur ce point: si cet effet secondaire est si important, alors faites-le se produire dans une ligne de code distincte.

Cependant, en termes d'efficacité, l'un ou l'autre est susceptible d'être meilleur. Le premier exécutera évidemment moins de code (n'évaluant pas du tout b dans un seul chemin de code), ce qui peut nous faire économiser le temps nécessaire pour évaluer b.

Le premier a cependant une autre branche. Considérez si nous le réécrivons dans notre langage hypothétique de style C sans &&:

if(a)
{
  if(b)
  {
    // Do something
  }
}

Ce if supplémentaire est caché dans notre utilisation de &&, Mais il est toujours là. Et en tant que tel, c'est un cas où il y a une prédiction de branche et potentiellement une mauvaise prédiction de branche.

Pour cette raison, le code if(a & b) peut être plus efficace dans certains cas.

Ici, je dirais que if(a && b) est toujours la meilleure approche pour commencer: Plus commun, la seule viable dans certains cas (où b fera une erreur si a est faux) et plus rapide qu'autrement. Il convient de noter que if(a & b) est souvent une optimisation utile à ce sujet dans certains cas.

26
Jon Hanna

Vous pouvez aller plus loin, et dans certaines langues, c'est la façon idiomatique de faire les choses: vous pouvez également utiliser une évaluation de court-circuit en dehors des instructions conditionnelles, et elles deviennent elles-mêmes une forme d'énoncé conditionnel. Par exemple. en Perl, il est idiomatique que les fonctions retournent quelque chose de faux en cas d'échec (et quelque chose de vrai en cas de succès), et quelque chose comme

open($handle, "<", "filename.txt") or die "Couldn't open file!";

est idiomatique. open renvoie quelque chose de différent de zéro en cas de succès (afin que la partie die après or ne se produise jamais), mais non définie en cas d'échec, puis l'appel à die ne se produire (c'est la façon de Perl de lever une exception).

C'est bien dans les langues où tout le monde le fait.

10
RemcoGerlich

Bien que je sois généralement d'accord avec réponse de dan1111 , il y a un cas particulièrement important qu'il ne couvre pas: le cas où smartphone est utilisé simultanément. Dans ce scénario, ce type de modèle est une source bien connue de bogues difficiles à trouver.

Le problème est que l'évaluation des courts-circuits n'est pas atomique. Votre thread peut vérifier que smartphone est null, un autre thread peut entrer et l'annuler, puis votre thread essaie rapidement de GetSignal() et explose.

Mais bien sûr, cela ne se produit que lorsque les étoiles s'alignent et que le timing de vos threads est juste juste.

Donc, bien que ce type de code soit très courant et utile, il est important de connaître cette mise en garde afin que vous puissiez éviter avec précision ce bogue désagréable.

6
Telastyn

Il y a beaucoup de situations où je veux d'abord vérifier une condition et ne veux vérifier une deuxième condition que si la première condition a réussi. Parfois purement pour l'efficacité (car il est inutile de vérifier la deuxième condition si la première a déjà échoué), parfois parce que sinon mon programme planterait (votre vérification pour NULL d'abord), parfois parce que sinon les résultats seraient horribles. (SI (je veux imprimer un document) ET (l'impression d'un document a échoué)) PUIS afficher un message d'erreur - vous ne voulez pas imprimer le document et vérifier si l'impression a échoué si vous ne vouliez pas imprimer un document dans le première place!

Il y avait donc un besoin ÉNORME pour garantir une évaluation en court-circuit des expressions logiques. Ce n'était pas présent dans Pascal (qui avait une évaluation de court-circuit non garantie) ce qui était une douleur majeure; Je l'ai vu pour la première fois dans Ada et C.

Si vous pensez vraiment que c'est une "mauvaise pratique", alors veuillez prendre n'importe quel morceau de code modérément complexe, le réécrire afin qu'il fonctionne correctement sans évaluation de court-circuit, et revenir nous dire comment vous l'avez aimé. C'est comme se faire dire que la respiration produit du dioxyde de carbone et demander si la respiration est une mauvaise pratique. Essayez sans lui pendant quelques minutes.

3
gnasher729

Une considération, non mentionnée dans d'autres réponses: parfois, avoir ces vérifications peut suggérer une refactorisation possible du modèle de conception Null Object . Par exemple:

if (currentUser && currentUser.isAdministrator()) 
  doSomething();

Pourrait être simplifié simplement:

if (currentUser.isAdministrator())
  doSomething ();

Si currentUser est défini par défaut sur un "utilisateur anonyme" ou "utilisateur nul" avec une implémentation de secours si l'utilisateur n'est pas connecté.

Pas toujours une odeur de code, mais quelque chose à considérer.

2
Pete