Cours complet pour apprendre à programmer en D


précédentsommairesuivant

44. assert et enforce

Dans les deux chapitres précédents, nous avons vu comment les exceptions et les instructions scope sont utilisées pour améliorer la justesse des programmes. assert est aussi un outil puissant qui poursuit le même but, en s'assurant que certaines suppositions sur lesquelles le programme est basé sont valides.

Il peut parfois être difficile de choisir entre lever une exception et utiliser assert. On utilisera assert dans tous les exemples suivants sans grande justification. On verra les différences plus tard dans le chapitre.

Même si ce n'est pas toujours évident, les programmes sont pleins de suppositions. Par exemple, la fonction suivante est écrite en faisant l'hypothèse que les deux paramètres d'âge sont supérieurs ou égaux à zéro :

 
Sélectionnez
double ageMoyen(double premier, double second)
{
    return (second + second) / 2;
}

Même s'il peut être incorrect pour le programme de rencontrer un âge qui est négatif, la fonction produirait quand même une moyenne, qui peut être utilisée dans le programme sans que personne ne le remarque, ce qui fait que le programme continue avec des données incorrectes.

Un autre exemple est la fonction suivante qui suppose qu'elle sera toujours appelée avec une des deux commandes « chanter » ou « danser » :

 
Sélectionnez
void executerCommande(string commande)
{
    if (commande == "chanter") {
        robotChanter();
 
    } else {
        robotDanser();
    }
}

À cause de cette supposition, la fonction robotDanser sera appelée pour toute commande autre que « chanter », correcte ou incorrecte.

Quand de telles suppositions n'existent que dans la tête du programmeur, le programme peut finir par fonctionner de façon incorrecte. assert vérifie les suppositions et termine le programme immédiatement quand elles sont incorrectes.

44-1. Syntaxe

assert peut être utilisé de deux manières :

 
Sélectionnez
assert(expression_logique);
assert(expression_logique, message);

L'expression logique représente une supposition sur le programme. assert évalue cette expression pour valider cette supposition. Si la valeur de l'expression logique est vraie, alors la supposition est considérée comme valide. Sinon, la supposition est invalide et une exception AssertError est levée.

Comme son nom le suggère, cette exception hérite d'Error, et comme nous l'avons vu dans le chapitre sur les ExceptionsExceptions, les exceptions qui héritent de Error ne doivent pas être attrapées. Il est important que le programme se termine au lieu de continuer avec des hypothèses invalides.

Les deux suppositions implicites de la fonction ageMoyen() que l'on vient de voir peuvent être écrites avec deux appels à assert :

 
Sélectionnez
double ageMoyen(double premier, double second)
{
    assert(premier >= 0);
    assert(second >= 0);
 
    return (premier + second) / 2;
}
 
void main()
{
    auto resultat = ageMoyen(-1, 10);
}

Ces vérifications avec assert portent le sens de « on suppose que les deux âges sont supérieurs ou égaux à zéro. ». On peut aussi le voir comme « cette fonction ne peut marcher correctement que si les deux âges sont supérieurs ou égaux à zéro ».

assert vérifie ces suppositions et termine le programme avec une AssertError quand elles ne sont pas valides :

 
Sélectionnez
core.exception.AssertError@essai(3): Assertion failure

La partie après l'arobase dans le message indique le fichier source et le numéro de la ligne de l'assertion qui a échoué. Selon la sortie ci-dessus, l'assertion qui a échoué est à la ligne 3 du fichier essai.d.

L'autre syntaxe de assert permet d'afficher un message personnalisé quand l'assertion échoue :

 
Sélectionnez
assert(premier >= 0, "L'âge ne peut pas être négatif.");

La sortie :

 
Sélectionnez
core.exception.AssertError@essai.d(3): L'âge ne peut pas être négatif.

Parfois, on pense qu'il est impossible pour un programme de rentrer dans un bloc de code donné. Dans de tels cas, il est courant d'utiliser le littéral false comme expression logique pour faire échouer une assertion. Par exemple, pour indiquer qu'on ne s'attend pas à ce que la fonction executerCommande() soit appelée avec autre chose que « chanter » ou « danser », et pour se protéger d'une telle possibilité, assert(false) peut être inséré dans la branche impossible :

 
Sélectionnez
void executerCommande(string commande)
{
    if (commande == "chanter") {
        robotChanter();
 
    } else if (commande == "dancer") {
        robotDanser();
 
    } else {
        assert(false);
    }
}

On garantit que la fonction ne fonctionne qu'avec les deux commandes qu'elle connaît. Note : une autre possibilité aurait été d'utiliser un final switch.

44-2. static assert

Comme les assertions sont là pour vérifier l'exécution correcte du programme, elles sont testées quand le programme est en fonctionnement. D'autres vérifications s'appliquent à la structure du programme et peuvent être vérifiées lors de sa compilation elle-même.

static assert est l'équivalent d'assert, mais appliqué lors de la compilation. L'avantage est qu'un programme qui aurait autrement eu un comportement incorrect à l'exécution ne peut ainsi même pas compiler. Un prérequis naturel est qu'il doit être possible d'évaluer l'expression logique lors de la compilation.

Par exemple, en supposant que le titre d'un menu soit affiché sur un périphérique de sortie qui a une largeur limitée, le static assert suivant s'assure qu'il ne sera jamais plus large que cette limite :

 
Sélectionnez
enum dstring titreMenu = "Menu Commande";
static assert(titreMenu.length <= 16);

Notez que la chaîne est définie comme enum pour que sa taille puisse être évaluée lors de la compilation.

Supposons qu'un programmeur change ce titre pour le rendre plus descriptif :

 
Sélectionnez
enum dstring titreMenu = "Menu des Commandes Directionnelles";
static assert(titreMenu.length <= 16);

L'assertion statique empêche la compilation du programme :

 
Sélectionnez
Error: static assert  (34u <= 16u) is false

Ceci rappelle au programmeur la limitation du périphérique de sortie.

static assert est encore plus utile lorsqu'on l'utilise avec les modèles (templates). Nous verrons les modèles dans des chapitres ultérieurs.

44-3. Utilisez les assertions même si c'est absolument vrai

J'insiste sur le « absolument vrai » parce que, de toute façon, on ne s'attend jamais à ce que les suppositions sur le programme soient fausses. Une grande partie des erreurs de programmation viennent de suppositions qu'on pensait absolument vraies.

Pour cette raison, utilisez des assertions même si cela ne semble pas nécessaire. Regardons la fonction suivante qui retourne le nombre de jours des mois d'une année donnée :

 
Sélectionnez
int[] joursMois(in int annee)
{
    int[] jours = [
        31, joursFevrier(annee),
        31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    ];
 
    assert((sum(jours) == 365) ||
           (sum(jours) == 366));
 
    return jours;
}

Cette assertion peut paraître inutile parce que la fonction retourne naturellement soit 365, soit 366. Cependant, ces vérifications protègent également contre les erreurs potentielles dans la fonction joursFevrier. Par exemple, le programme se terminerait si joursFevrier retournait 30.

Une autre vérification paraissant inutile peut vérifier que la taille de la tranche est toujours 12 :

 
Sélectionnez
assert(jours.length == 12);

De cette manière, si on supprime ou on ajoute des éléments dans la tranche involontairement, cela sera détecté. De telles vérifications sont des outils importants vis-à-vis de la correction des programmes.

assert est aussi l'outil de base qui est utilisé dans les tests unitaires et la programmation par contrat, que nous verrons dans des chapitres ultérieurs.

44-4. Pas de valeur ni d'effet de bord

Nous avons vu que les expressions produisent des valeurs ou ont des effets de bord. Les assertions n'ont pas de valeur et ne devraient pas avoir d'effet de bord.

Le langage D requiert que l'évaluation de l'expression logique ne doive pas avoir d'effet de bord. assert doit rester un observateur passif de l'état du programme.

44-5. Désactiver les assertions

Comme les assertions concernent la correction des programmes, elles peuvent sembler inutiles dès lors que le programme a été testé suffisamment. De plus, comme les assertions ne produisent pas de valeur et qu'elles n'ont pas d'effet de bord, les supprimer du programme ne devrait pas changer le comportement du programme.

L'option -release de dmd permet d'ignorer les assertions, comme si elles n'avaient jamais été écrites dans le programme :

 
Sélectionnez
dmd essai.d -release

NDT Pour gdc, vous pouvez utiliser l'option -frelease ou -fno-assert.

Cela devrait permettre aux programmes de s'exécuter plus vite en évitant d'évaluer des expressions logiques potentiellement lentes dans les assertions.

En revanche, les assertions qui ont le littéral false ou 0 comme expression logique ne sont pas désactivées même quand le programme est compilé avec ces options. En effet, assert(false) est là pour s'assurer qu'un bloc de code n'est jamais atteint, et cela doit être le cas même pour une version en mode release.

44-6. enforce pour lever des exceptions

Toutes les situations inattendues ne signifient pas une erreur de programmation. Les programmes peuvent aussi rencontrer des entrées ou des états de l'environnement inattendus. Par exemple, une donnée qui est entrée par l'utilisateur ne devrait pas être vérifiée en utilisant une assertion, parce qu'une donnée invalide n'a rien à voir avec la correction du programme lui-même. Dans de tels cas, lever une exception comme nous l'avons fait dans les programmes précédents est plus approprié.

std.exception.enforce est une manière commode de lever des exceptions. Par exemple, supposons qu'une exception doive être levée quand une condition donnée n'est pas remplie :

 
Sélectionnez
if (nombre < 3) {
  throw new Exception("Doit être supérieur ou égal à 3.");
}

enforce fait une vérification et lève une exception si elle n'est pas vérifiée. Le code suivant est l'équivalent du code précédent :

 
Sélectionnez
import std.exception;
// ...
    enforce(nombre >= 3, "Doit être supérieur ou égal à 3.");

Notez que l'expression logique est inversée par rapport à l'instruction if, puisqu'elle traduit ce qui est forcé.

44-7. Utilisation

assert est là pour détecter des erreurs du programmeur. Les conditions que vérifie assert dans la fonction joursMois, et celles sur la variable titreMenu, vues plus tôt dans ce chapitre, concernant les erreurs du programmeur.

Parfois, il est difficile de choisir entre utiliser assert ou lever une exception. La décision devrait être basée sur le fait que la situation inattendue vienne d'un problème dans le code du programme, ou non.

Si ce n'est pas le cas, le programme doit lever une exception quand il n'est pas possible d'accomplir une tâche. enforce est expressif et commode pour lever des exceptions.

Un autre point à considérer est la possibilité de remédier à la situation d'une quelconque manière. Si le programme peut faire quelque chose, même simplement afficher un message d'erreur sur le problème avec des données entrées, il est approprié de lever une exception. De cette manière, les appelants du code qui a levé l'exception peuvent l'attraper et gérer l'erreur.

44-8. Exercices

  1. Le programme suivant inclut plusieurs assertions. Compilez et exécutez le programme pour découvrir les bogues qui sont révélés par ces assertions.
    Le programme demande un moment de départ et une durée à l'utilisateur et calcule le moment de fin en ajoutant la durée au moment de départ :

     
    Sélectionnez
    10 heures et 8 minutes après 06:09 donne 16:17.
  2. Notez que ce problème pourrait être écrit d'une manière beaucoup plus propre en définissant des structures. Nous reprendrons ce programme dans des chapitres ultérieurs.

     
    Sélectionnez
    import std.stdio;
    import std.string;
    import std.exception;
     
    /* Récupère le moment en tant qu'heures et minutes après avoir affiché
     * un message. */
    void lireMoment(in string message, out int heures, out int minutes)
    {
        write(message, "? (HH:MM) ");
     
        readf(" %s:%s", &heures, &minutes);
     
        enforce((heures >= 0) && (heures <= 23) &&
                (minutes >= 0) && (minutes <= 59),
                "moment invalide !");
    }
     
    /* Retourne le moment en tant que chaîne de caractères. */
    string momentVersChaine(in int heures, in int minutes)
    {
        assert((heures >= 0) && (heures <= 23));
        assert((minutes >= 0) && (minutes <= 59));
     
        return format("%02s:%02s", heures, minutes);
    }
     
    /* Ajoute la durée au moment de départ et retourne le résultat dans
     * la troisième paire de paramètres */
    void ajouterDuree(in int heuresDepart, in int minutesDepart,
                      in int dureeHeures, in int dureeMinutes,
                      out int heuresResultat, out int minutesResultat)
    {
        heuresResultat = heuresDepart + dureeHeures;
        minutesResultat = minutesDepart + dureeMinutes;
     
        if (minutesResultat > 59) {
            ++heuresResultat;
        }
    }
     
    void main()
    {
        int heuresDepart;
        int minutesDepart;
        lireMoment("Moment de départ", minutesDepart, heuresDepart);
     
        int dureeHeures;
        int dureeMinutes;
        lireMoment("Durée", dureeHeures, dureeMinutes);
     
        int heuresFin;
        int minutesFin;
        ajouterDuree(heuresDepart, minutesDepart,
                    dureeHeures, dureeMinutes,
                    heuresFin, minutesFin);
     
        writefln("%s heures et %s minutes après %s donne %s.",
                 dureeHeures, dureeMinutes,
                 momentVersChaine(heuresDepart, minutesDepart),
                 momentVersChaine(heuresFin, minutesFin));
    }
  3. Lancez le programme et entrez 06:09 comme moment de départ et 1:2 comme durée. Observez que le programme termine normalement.
    Note : il se peut que vous remarquiez un problème avec la sortie. Ignorez ce problème, vous le découvrirez avec l'aide des assertions très bientôt.

  4. Cette fois, entrez 06:09 et 15:2. Observez que le programme se termine par une AssertError. Allez à la ligne du programme qui est indiquée dans le message de l'assertion et voyez qu'une des assertions a échoué. Découvrir la cause de cet échec particulier peut prendre du temps.

  5. Entrez 06:09 et 20:0 ; observez que la même assertion échoue encore et corrigez aussi ce bogue.

  6. Modifiez le programme pour afficher les temps au format 12 heures avec des indicateurs « am » (avant midi) et « pm » (après midi).

Les solutionsassert et enforce - Correction.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+