programmez-en-d/assert.whata

331 lines
16 KiB
Plaintext

[set
title = "[c assert] et [c enforce]"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Dans les deux chapitres précédents, nous avons vu comment les exceptions et les instructions [c scope] sont utilisées pour améliorer la justesse des programmes. [c 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 [c assert]. On utilisera [c 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'âges sont supérieurs ou égaux à zéro~ :
[code=d <<<
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 «~ dancer~ »~ :
[code=d <<<
void executerCommande(string commande)
{
if (commande == "chanter") {
robotChanter();
} else {
robotDancer();
}
}
>>>]
À cause de cette supposition, la fonction [c robotDance] 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. [c assert] vérifie les suppositions et termine le programme immédiatement quand elles sont incorrectes.
[ = Syntaxe
[c assert] peut être utilisé de deux manières~ :
[code=d <<<
assert(expression_logique);
assert(expression_logique, message);
>>>]
L'expression logique représente une supposition sur le programme. [c 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 [c AssertError] est levée.
Comme son nom le suggère, cette exception hérite d'[c Error], et comme nous l'avons vu dans le [[part:exceptions | chapitre sur les Exceptions]], les exceptions qui héritent de [c 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 [c ageMoyen()] que l'on vient de voir peuvent être écrites avec deux appels à [c assert]~ :
[code=d <<<
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 [c 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~ ».
[c assert] vérifie ces suppositions et termine le programme avec une [c AssertError] quand elles ne sont pas valides~ :
[output <<<
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 [c essai.d].
L'autre syntaxe de [c assert] permet d'afficher un message personnalisé quand l'assertion échoue~ :
[code=d <<<
assert(premier >= 0, "L'âge ne peut pas être négatif.");
>>>]
La sortie~ :
[output <<<
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 [c false] comme expression logique pour faire échouer une assertion. Par exemple, pour indiquer qu'on ne s'attend pas à ce que la fonction [c executerCommande()] soit appelée avec autre chose que «~ chanter~ » ou «~ danser~ », et pour se protéger d'une telle possibilité, [c assert(false)] peut être inséré dans la branche [* impossible]~ :
[code=d <<<
void executerCommande(string commande)
{
if (commande == "chanter") {
robotChanter();
} else if (commande == "dancer") {
robotDancer();
} 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 [c final switch].
]
[ = [c static assert]
Comme les assertions sont là pour vérifier l'exécution correcte du programme, elle 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.
[c static assert] est l'équivalent d'[c 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 [c static assert] suivant s'assure qu'il ne sera jamais plus large que cette limite~ :
[code=d <<<
enum dstring titreMenu = "Menu Commande";
static assert(titreMenu.length <= 16);
>>>]
Notez que la chaîne est définie comme [c 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~ :
[code=d <<<
enum dstring titreMenu = "Menu des Commandes Directionnelles";
static assert(titreMenu.length <= 16);
>>>]
L'assertion statique empêche la compilation du programme~ :
[output <<<
Error: static assert (34u <= 16u) is false
>>>]
Ceci rappelle au programmeur la limitation du périphérique de sortie.
[c 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.
]
[ = Utilisez les assertions même si c'est absolument vrai
J'insiste sur le «~ absolument vrai~ » parce que, de toutes façons, on ne s'attend jamais à ce que les suppositions sur le programmes soient fausses. Une grande partie des erreurs de programmations 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 les jours des mois d'une année donnée~ :
[code=d <<<
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 [c joursFevrier]. Par exemple, le programme se terminerait si [c joursFevrier] retournait [c 30].
Une autre vérification paraissant inutile peut vérifier que la taille de la tranche est toujours 12~ :
[code=d <<<
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.
[c 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.
]
[ = Pas de valeur ni d'effet de bord
Nous avons vu que les expressions produisent des valeurs ou ont des effets de bords. 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 doit pas avoir d'effet de bord. [c assert] doit rester un observateur passif de l'état du programme.
]
[ = Désactiver les assertions
Comme les assertions concernent la correction des programmes, elle 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 [c -release] de [c dmd] permet d'ignorer les assertions, comme si elles n'avaient jamais été écrites dans le programme~ :
[code=bash <<<
dmd essai.d -release
>>>]
[p NdT~ : | pour [c gdc], vous pouvez utiliser l'option [c -frelease] ou [c -fno-assert].]
Cela devrait permettre aux programme 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 [c false] ou [c 0] comme expression logique ne sont pas désactivées même quand le programme est compilé avec ces options. En effet, [c 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''.
]
[ = [c 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érifée en utilisant une assertion, parce qu'une donnée invalide n'a rien avoir 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é.
[c 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~ :
[code=d <<<
if (nombre < 3) {
throw new Exception("Doit être supérieur ou égal à 3.");
}
>>>]
[c 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~ :
[code=d <<<
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 [c if], puisqu'elle traduit ce qui est [* forcé].
]
[ = Utilisation
[c assert] est là pour détecter des erreurs du programmeur. Les conditions que vérifie [c assert] dans la fonction [c joursMois], et celles sur la variable [c titreMenu], vues plus tôt dans ce chapitre, concernent les erreurs du programmeur.
Parfois, il est difficile de choisir entre utiliser [c 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. [c 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.
]
[ = Exercices
# [
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~ :
[output <<<
10 heures et 8 minutes après 06:09 donne 16:17.
>>>]
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.
[code=d <<<
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));
}
>>>]
Lancez le programme et entrez [c 06:09] comme moment de départ et [c 1:2] comme durée. Observez que le programme termine normalement.
[p 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.]
]
# [
Cette fois, entrez [c 06:09] et [c 15:2]. Observez que le programme se termine par une [c 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.
]
# [
Entrez [c 06:09] et [c 20:0]~ ; observez que la même assertion échoue encore et corrigez aussi ce bogue.
]
# [
Modifiez le programme pour afficher les temps au format 12 heures avec des indicateurs «~ am~ » (avant midi) et «~ pm~ » (après midi).
]
[[part:corrections/assert | Les solutions]]
]