programmez-en-d/exceptions.whata

823 lines
36 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Exceptions"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Les situations non attendues font partie des programmes~ : erreurs de l'utilisateur, erreurs de programmation, changements dans l'environnement de programmation, etc. Les programmes doivent être écrits d'une manière qui évite la production de résultats incorrects quand on se trouve face à de telles conditions [i exceptionnelles].
Certaines de ces conditions peuvent être suffisamment graves pour stopper l'exécution du programme. Par exemple, une information nécessaire peut manquer ou être invalide, ou un périphérique peut ne pas fonctionner correctement. Le mécanisme de gestion des exceptions du D aide à stopper l'exécution du programme lorsque c'est nécessaire et à récupérer d'une situation inattendue quand c'est possible.
Comme exemple de situation inattendue, on peut penser au cas où l'on passe un opérateur inconnu à une fonction qui ne connaît que les 4 opérateurs arithmétiques, comme nous l'avons vu dans les exercices d'un chapitre précédent~ :
[code=d <<<
switch (opérateur) {
case "+":
writeln(premier + second);
break;
case "-":
writeln(premier - second);
break;
case "x":
writeln(premier * second);
break;
case "/":
writeln(premier / second);
break;
default:
throw new Exception(format("Opérateur invalide : %s", opérateur));
}
>>>]
L'instruction [c switch] ci-dessus ne sait pas quoi faire avec les opérateurs qui ne sont pas listés dans les instructions [c case], elle lève donc une exception.
Il y a plein d'exemples d'expressions levées dans Phobos. Par exemple, [c to!int], qui peut être utilisé pour convertir une représentation textuelle d'un entier à une valeur [c int] lève une exception quand cette représentation n'est pas valide~ :
[code=d <<<
import std.conv;
void main()
{
const int value = to!int("salut");
}
>>>]
Le programme termine avec une exception levée par [c to!int]~ :
[code <<<
std.conv.ConvException@/usr/include/d/4.8/std/conv.d(1826): Unexpected 's' when converting from type string to type int
>>>]
[c std.conv.ConvException] au début du message est le type de l'objet exception levé. D'après ce nom, on peut dire que le type est [c ConvException], qui est défini dans le module [c std.conv].
[ = L'instruction [c throw] pour lever des exceptions
Nous avons vu l'instruction [c throw] aussi bien dans les exemples ci-dessus que dans des chapitres précédents.
[c throw] lève un [* objet exception] et cela termine l'opération courante du programme. Les expressions et les instructions qui sont écrites après l'instruction [c throw] ne sont pas exécutées. Ce comportement correspond à la nature des exceptions~ : elle doivent être levées quand le programme ne peut pas continuer sa tâche courante.
À l'inverse, si le programme pouvait continuer, alors la situation ne justifierait pas l'utilisation d'une exception. Dans de tels cas, la fonction trouverait une solution et continuerait.
[ = Les types d'exceptions [c Exception] et [c Error]
Seuls les types qui sont hérités de la classe [c Throwable] peuvent être levés. [c Throwable] n'est quasiment jamais directement utilisée dans les programmes. En pratique, les types qui sont levés sont des types qui héritent d'[c Exception] ou d'[c Error], qui sont eux-mêmes des types qui héritent de [c Throwable]. Par exemple, toutes les exceptions que Phobos lève sont héritées de [c Exception] ou [c Error].
[c Error] représente des situations irrécupérables qu'il n'est pas recommandé d'attraper. Pour cette raison, la plupart des exceptions qu'un programme lève sont d'un type hérité de [c Exception]. L'héritage est un concept relatif aux classes. Nous verrons les classes dans un chapitre ultérieur.
Les objets de type [c Exception] sont construits à partir d'une valeur [c string] qui représente un message d'erreur. Vous pouvez éventuellement utiliser la fonction [c format] du module [c std.string] pour créer ce message~ :
[code=d <<<
import std.stdio;
import std.random;
import std.string;
int[] ValeursDeDesAleatoires(int nombre)
{
if (nombre < 0) {
throw new Exception(
format("Nombre de dés invalide : %s", nombre));
}
int[] valeurs;
foreach (i; 0 .. nombre) {
valeurs ~= uniform(1, 7);
}
return valeurs;
}
void main()
{
writeln(ValeursDeDesAleatoires(-5));
}
>>>]
[output <<<
object.Exception...: Nombre de dés invalide : -5
>>>]
Dans la plupart des cas, au lieu de créer un object [c Exception] explicitement avec [c new] et lever l'exception avec [c throw], la fonction [c enforce()] est appelée. Par exemple, l'équivalent de la vérification d'erreur précédente est l'appel suivant à [c enforce()]~ :
[code=d <<<
enforce(nombre >= 0, format("Nombre de dés invalide : %s", nombre));
>>>]
Nous verrons les différences entre [c enforce()] et [c assert()] dans un chapitre ultérieur.
]
[ = Les expressions levées mettent fin à toutes les portées
Nous avons vu que l'exécution du programme commence par la fonction [c main] et entre dans les autres fonctions à partir de là. Cette exécution par étapes, entrant progressivement en profondeur dans les fonctions et en ressortant, peut être vue comme les branches d'un arbre.
Par exemple, [c main()] peut appeler une fonction nommée [c faireOmelette()], qui peut à son tour appeler une autre fonction nommée [c toutPreparer()], qui peut à son tour appeler une autre fonction nommée [c preparerOeufs()], etc. En supposant que les flèches indiquent des appels, l'arborescence d'un tel programme peut être représentée comme dans cet arbre d'appels de fonctions~ :
[code <<<
main
|
+--▶ faireOmelette
| |
| +--▶ toutPreparer
| | |
| | +-▶ preparerOeufs
| | +-▶ preparerBeurre
| | +-▶ preparerPoelle
| |
| +--▶ cuisinerOeufs
| +--▶ toutNettoyer
|
+--▶ mangerOmelette
>>>]
Le programme suivant montre son arborescence en utilisant différents niveaux d'indentation dans sa sortie. Le programme n'a d'autre utilité que de produire une sortie qui correspond à ce que nous voulons illustrer~ :
[code=d <<<
import std.stdio;
void indenter(in int niveau)
{
foreach (i; 0 .. niveau * 2) {
write(' ');
}
}
void entrant(in char[] nomFonction, in int niveau)
{
indenter(niveau);
writeln("▶ Première ligne de ", nomFonction);
}
void sortant(in char[] nomFonction, in int niveau)
{
indenter(niveau);
writeln("◁ Dernière ligne de ", nomFonctionx);
}
void main()
{
entrant("main", 0);
faireOmelette();
mangerOmelette();
sortant("main", 0);
}
void faireOmelette()
{
entrant("faireOmelette", 1);
toutPreparer();
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
}
void mangerOmelette()
{
entrant("mangerOmelette", 1);
sortant("mangerOmelette", 1);
}
void toutPreparer()
{
entrant("toutPreparer", 2);
preparerOeufs();
preparerBeurre();
preparerPoelle();
sortant("toutPreparer", 2);
}
void cuisinerOeufs()
{
entrant("cuisinerOeufs", 2);
sortant("cuisinerOeufs", 2);
}
void toutNettoyer()
{
entrant("toutNettoyer", 2);
sortant("toutNettoyer", 2);
}
void preparerOeufs()
{
entrant("preparerOeufs", 3);
sortant("preparerOeufs", 3);
}
void preparerBeurre()
{
entrant("preparerBeurre", 3);
sortant("preparerBeurre", 3);
}
void preparerPoelle()
{
entrant("preparerPoelle", 3);
sortant("preparerPoelle", 3);
}
>>>]
Le programme produit la sortie suivante :
[output <<<
▶ Première ligne de main
▶ Première ligne de faireOmelette
▶ Première ligne de toutPreparer
▶ Première ligne de preparerOeufs
◁ Dernière ligne de preparerOeufs
▶ Première ligne de preparerBeurre
◁ Dernière ligne de preparerBeurre
▶ Première ligne de preparerPoelle
◁ Dernière ligne de preparerPoelle
◁ Dernière ligne de toutPreparer
▶ Première ligne de cuisinerOeufs
◁ Dernière ligne de cuisinerOeufs
▶ Première ligne de toutNettoyer
◁ Dernière ligne de toutNettoyer
◁ Dernière ligne de faireOmelette
▶ Première ligne de mangerOmelette
◁ Dernière ligne de mangerOmelette
◁ Dernière ligne de main
>>>]
Les fonctions [c entrant] et [c sortant] sont utilisées pour indiquer les première et dernière lignes des fonctions avec l'aide des caractères [c ▶] et [c ◁]. Le programme commence par la première ligne de [c main()], entre dans les autres fonctions et finit par la dernière ligne de [c main()].
Modifions la fonction [c preparerOeufs()] pour prendre le nombre d'œufs en paramètre. Comme certaines valeurs de ce paramètre seraient des erreurs, faisons en sorte que cette fonction lève une exception quand le nombre d'œufs est inférieur à 1~ :
[code=d <<<
void preparerOeufs(int nombre)
{
entrant("preparerOeufs", 3);
if (nombre < 1) {
throw new Exception(
format("Impossible de prendre %s oeufs du réfrigérateur", nombre));
}
sortant("preparerOeufs", 3);
}
>>>]
Afin de pouvoir compiler le programme, nous devons modifier d'autres lignes du programme pour les rendre compatibles avec cette modification. Le nombre d'œufs à prendre du réfrigérateur peut être passé de fonction en fonction, en commençant par [c main()]. Les parties du programme qui doivent changer sont les suivantes. La valeur invalide de [c -8] est voulue, elle est là pour montrer ce qui change dans la sortie du programme par rapport à la sortie précédente, quand une exception est levée~ :
[code=d <<<
// ...
void main()
{
entrant("main", 0);
faireOmelette(-8);
mangerOmelette();
sortant("main", 0);
}
void faireOmelette(int nombreOeufs)
{
entrant("faireOmelette", 1);
toutPreparer(nombreOeufs);
cuisinerOeufs();
toutNettoyer();
sortant("faireOmelette", 1);
}
// ...
void toutPreparer(int nombreOeufs)
{
entrant("toutPreparer", 2);
preparerOeufs(nombreOeufs);
preparerBeurre();
preparerPoelle();
sortant("toutPreparer", 2);
}
// ...
>>>]
Maintenant, quand on démarre le programme, on voit qu'il manque les lignes qui étaient affichées après le point où l'exception est levée~ :
[output <<<
▶ Première ligne de main
▶ Première ligne de faireOmelette
▶ Première ligne de toutPreparer
▶ Première ligne de preparerOeufs
object.Exception: Impossible de prendre -8 oeufs du réfrigérateur
>>>]
Quand l'exception est levée, l'exécution du programme sort des fonctions [c preparerOeufs()], [c toutPreparer()], [c faireOmelette()] et [c main()] dans cet ordre, du niveau le plus profond au niveau le moins profond. Aucune étape additionnelle n'est exécutée quand le programme sort de ces fonctions.
Un arrêt si brutal est justifié par le fait qu'on devrait considérer qu'un échec dans une fonction de plus bas niveau implique que les fonctions de plus hauts niveaux, nécessitant un succès de celle-ci, ont également échoué.
L'objet exception qui est levé depuis un niveau de fonction plus bas est transféré aux fonctions de niveaux supérieurs, niveau par niveau, pour finalement entraîner la sortie du programme de la fonction [c main()]. Le chemin que l'exception prend peut être vu comme le chemin rouge dans l'arbre suivant :
[img=exception_rouge.svg | chemin de l'exception dans l'arbre d'exécution]
Le but du mécanisme des exceptions est précisément d'avoir ce comportement~ : sortir de tous les appels de fonctions directment. Parfois, ''attraper'' l'exception levée pour trouver une manière de continuer l'exécution du programme fait sens. Nous allons bientôt introduire le mot-clé [c catch].
[ = Quand utiliser [c throw]
Utilisez [c throw] dans les situations où il n'est pas possible de continuer. Par exemple, une fonction qui lit un nombre d'étudiants depuis un fichier peut lever une exception si l'information n'est pas disponible ou si elle est incorrecte.
D'un autre côté, si le problème est causé par une action quelconque de l'utilisateur telle que la saisie d'une valeur invalide, il peut être plus judicieux de valider cette donnée au lieu de lever une exception. Afficher un message d'erreur et demander à l'utilisateur de saisir une nouvelle fois la donnée est plus approprié dans ce genre de cas.
]
]
[ = L'instruction [c try-catch] pour attraper une exception
Comme nous l'avons vu précédement, une exception levée entraîne la sortie de toutes les fonctions et, au final, l'arrêt du programme entier.
L'objet exception peut être attrapé avec une instruction [c try-catch] à n'importe quel endroit du chemin qu'elle emprunte pour sortir des fonctions. L'instruction [c try-catch] modélise la phrase «~ [* essaie] de faire quelque chose et [* attrape] les exceptions qui peuvent être levées.~ ». Voici la syntaxe de [c try-catch]~ :
[code=d <<<
try {
// Le bloc de code qui est exécuté, où une
// exception peut être levée
} catch (un_type_d_exception)) {
// Expressions à exécuter si une exception de ce
// type est attrapée
} catch (un_autre_type_d_exception)) {
// Expressions à exécuter si une exception de cet
// autre type est attrapée
// ... d'autres blocs catch peuvent être placés ici ...
} finally {
// Expressions à exécuter indépendamment du fait
// qu'une exception soit levée ou pas
}
>>>]
Commençons par le programme suivant qui n'utilise pas d'instruction [c try-catch]. Le programme lit une valeur d'un dé depuis un fichier et l'affiche sur la sortie standard~ :
[code=d <<<
import std.stdio;
int lireDeDepuisFichier()
{
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
int de;
fichier.readf(" %s", &de);
return de;
}
void main()
{
const int de = lireDeDepuisFichier();
writeln("Valeur du dé : ", de);
}
>>>]
Notez que la fonction [c lireDeDepuisFichier()] est écrite de manière à ignorer les conditions d'erreurs, s'attendant à ce que le fichier et la valeur qu'il contient soient là. En d'autres termes, la fonction fait ce qu'elle doit faire sans s'occuper des conditions d'erreurs. C'est un des avantages des exceptions~ : beaucoup de fonctions peuvent être écrites en se concentrant sur leur tâche, plutôt qu'en se concentrant sur les conditions d'erreurs.
Lançons le programme alors que le fichier [c le_fichier_qui_contient_la_valeur] manque~ :
[output <<<
std.exception.ErrnoException@std/stdio.d(286): Cannot open
file `le_fichier_qui_contient_la_valeur' in mode `r' (No such
file or directory)
>>>]
Une exception du type [c ErrnoException] est levée et le programme se termine sans afficher «~ Valeur du  :~ ».
Ajoutons une fonction intermédiaire au programme qui appelle [c lireDeDepuisFichier()] depuis un bloc [c try] et [c main()] appelera cette nouvelle fonction~ :
[code=d <<<
import std.stdio;
int lireDeDepuisFichier()
{
auto fichier = File("le_fichier_qui_contient_la_valeur", "r");
int de;
fichier.readf(" %s", &de);
return de;
}
int essayerDeLireDepuisLeFichier()
{
int de;
try {
de = lireDeDepuisFichier();
} catch (std.exception.ErrnoException exc) {
writeln("Impossible de lire le fichier ; on suppose 1");
de = 1;
}
return de;
}
void main()
{
const int de = essayerDeLireDepuisLeFichier();
writeln("Valeur du dé : ", de);
}
>>>]
Quand on lance le programme alors que [c le_fichier_qui_contient_la_valeur] n'existe pas, le programme ne termine cette fois pas par une exception~ :
[output <<<
Impossible de lire depuis le fichier ; on suppose 1
Valeur du dé : 1
>>>]
Le nouveau programme essaie d'exécuter [c lireDeDepuisFichier()] dans un bloc [c try]. Si ce bloc s'exécute correctement, la fonction se termine normalement avec l'instruction [c return de;]. Si l'exécution du bloc [c try] finit avec l'exception [c std.exception.ErrnoException], alors le programme entre dans le bloc [c catch].
Voici un récapitulatif de ce qu'il se passe lorsque le programme est démarré alors que le fichier n'existe pas~ :
- comme dans le programme précédent, une exception [c std.exception.ErrnoException] est levée (par [c File()], pas par notre code)~ ;
- cette exception est attrapée par [c catch]~ ;
- la valeur de [c 1] est supposée pendant l'exécution normale du bloc [c catch]~ ;
- et le programme continue normalement.
[c catch] est là pour attraper les exceptions levées afin de tenter de continuer l'exécution du programme.
En guise d'exemple supplémentaire, revenons au programme des omelettes et ajoutons une instruction [c try-catch] à sa fonction [c main()]~ :
[code=d <<<
void main()
{
entrant("main", 0);
try {
faireOmelette(-8);
mangerOmelette();
} catch (Exception exc) {
write("Impossible de manger l'omelette : ");
writeln('"', exc.msg, '"');
writeln("Je mangerai chez le voisin...");
}
sortant("main", 0);
}
>>>]
Ce bloc [c try] contient deux lignes de codes. Toute exception levée depuis une de ces deux lignes sera attrapée par le bloc [c catch].
[output <<<
▶ main, première ligne
▶ faireOmelette, première ligne
▶ toutPreparer, première ligne
▶ preparerOeufs, première ligne
Impossible de manger l'omelette : "Impossible de prendre -8 oeufs du réfrigérateur"
Je mangerai chez le voisin...
◁ main, dernière ligne
>>>]
Comme nous pouvons le voir dans la sortie, le programme ne termine plus à cause de l'exception. Il se remet de son état d'erreur et continue à s'exécuter normalement jusqu'à la fin de la fonction [c main()].
]
[ = Les blocs [c catch] sont parcourus séquentiellement
Le type [c Exception], que nous avons utilisé jusqu'à maintenant dans les exemples, est un type générique d'exception. Ce type indique simplement qu'une erreur s'est produite dans le programme. Il contient aussi un message qui peut expliquer l'erreur plus en détail, mais il ne contient pas d'information sur le [* type] de l'erreur.
[c ConvException] et [c ErrnoException], que nous avons rencontrés plutôt dans le chapitre, sont des types d'exceptions plus spécifiques~ : le premier concerne une erreur de conversion et le second une erreur système. Comme beaucoup d'autres types d'exceptions dans Phobos et comme leurs noms respectifs le suggèrent, [c ConvException] et [c ErrnoException] héritent tous deux de la classe [c Exception].
[c Exception] et sa sœur [c Error] sont eux-même hérités de [c Throwable], le type d'exception le plus général.
Même si c'est possible, il n'est pas recommandé d'attraper des objets de type [c Error] ou de types qui en héritent. Comme [c Throwable] est plus général qu'[Error], il n'est pas non plus recommandé d'attraper [c Throwable] non plus. Les seuls objets qui devraient normalement être attrapés sont ceux qui font partie de la hiérarchie [c Exception].
[pre <<<
Throwable (attraper n'est pas recommandé)
↗ ↖
Exception Error (attraper n'est pas recommandé)
↗ ↖ ↗ ↖
... ... ... ...
>>>]
[p Note~ : | on verra la représentation hiérarchique plus tard dans le [[part:heritage | chapitre sur l'héritage]]. L'arbre précédent indique que [c Throwable] est la plus générale et qu'[c Exception] et [c Error] sont plus spécifiques.]
Il est possible d'attraper les objets d'un type particulier. Par exemple, il est possible d'attraper de manière spécifique un objet [c ErrnoException] pour gérer une erreur système.
Les exceptions sont attrapées seulement si elle correspondent au type qui est spécifié dans un bloc [c catch]. Par exemple, un bloc [c catch] qui essaie d'attraper une exception [c SpecialExceptionType] n'attrapera pas un [c ErrnoException].
Le type de l'objet exception qui est levée pendant l'exécution d'un bloc [c try] est comparé aux types qui sont spécifiés par les blocs [c catch], dans l'ordre dans lequel les blocs [c catch] sont écrits. Si le type de l'objet correspond au type d'un bloc [c catch], alors l'exception est considérée comme attrapée par ce bloc [c catch] et le code qui est à l'intérieur de ce bloc est exécuté. Une fois qu'une correspondance a été trouvée, les blocs [c catch] restant sont ignorés.
Du fait que les blocs [c catch] sont testés dans l'ordre, les blocs [c catch] doivent être ordonnées de l'exception du type le plus spécifique au plus général. De même, si nécessaire, le type [c Exception] doit être indiqué au dernier bloc [c catch].
Par exemple, une instruction [c try-catch] qui essaie d'attraper plusieurs types spécifiques d'exceptions à propos d'une liste d'étudiants peut ordonner les blocs [c catch] du plus spécifique au plus général comme dans le code qui suit~ :
[code=d <<<
try {
// des opérations sur les enregistrements d'étudiants
// qui peuvent lever une exception
} catch (ChiffreIdEtudiantException exc) {
// Une exception dédiée aux erreurs sur
// les chiffres des identifiants des étudiants
} catch (IdEtudiantException exc) {
// Une exceptions plus générale sur les identifiants des étudiants
// mais pas nécessairement sur leurs chiffres
} catch (EnregistrementEtudiantException exc) {
// Une exception plus générale sur les enregistrements des étudiants
} catch (Exception exc) {
// L'exception la plus générale qui peut ne pas être en
// rapport avec les enregistrements des étudiants
}
>>>]
]
[ = Le bloc [c finally]
[c finally] est un bloc optionnel de l'instruction [c try-catch]. Il contient des expressions qui doivent être exécutées, qu'une exception ait été levée ou pas.
Pour voir comment [c finally] fonctionne, examinons un programme qui lève une exception 50% du temps~ :
[code=d <<<
import std.stdio;
import std.random;
void leverExceptionLaMoitieDuTemps()
{
if (uniform(0, 2) == 1) {
throw new Exception("le message d'erreur");
}
}
void foo()
{
writeln("La première ligne de foo()");
try {
writeln("La première ligne du bloc try");
leverExceptionLaMoitieDuTemps();
writeln("La dernière ligne du bloc try");
// ... il peut y avoir un ou des blocs catch ici ...
} finally {
writeln("Le corps du bloc finally");
}
writeln("La dernière ligne de foo()");
}
void main()
{
foo();
}
>>>]
La sortie du programme est la suivante quand la fonction ne lève pas une exception~ :
[output <<<
La première ligne de foo()
La première ligne du bloc try
La dernière ligne du bloc try
Le corps du bloc finally
La dernière ligne de foo()
>>>]
La sortie du programme est la suivante quand la fonction lève une exception~ :
[output <<<
La première ligne de foo()
La première ligne du bloc try
Le corps du bloc finally
object.Exception@essai.d: le message d'erreur
>>>]
Comme on peut le constater, quand une exception est levée, bien que «~ La dernière ligne du bloc try~ » et «~ La dernière ligne de foo()~ » ne soient pas affichées, le contenu du bloc [c finally] est quant à lui toujours exécuté.
]
[ = Quand utiliser l'instruction [c try-catch]
L'instruction [c try-catch] est utile pour attraper des exceptions pour faire quelque chose de spécial avec.
Pour cette raison, l'instruction [c try-catch] ne devrait être utilisée que quand il y a quelque chose de spécial à faire. Sinon, n'attrapez pas d'exceptions et laissez-les aux fonctions des niveaux supérieurs qui pourraient vouloir les attraper.
]
]
[ = Propriétés des exceptions
L'information qui est automatiquement affichée dans la sortie quand le programme se termine à cause d'une exception est aussi accessible par les propriétés des objets [c Exception]. Ces propriétés sont fournies par l'interface [c Throwable]~ :
- [c .file] : Le fichier source à partir duquel l'exception a été levée
- [c .line] : Le numéro de la ligne à partir de laquelle l'exception a été levée
- [c .msg] : Le message d'erreur
- [c .info] : L'état de la pile du programme quand l'exception a été levée
- [c .next] : L'exception collatérale suivante
Nous avons vu que les blocs [c finally] sont exécutés quand on sort des blocs de code, y compris à cause des exceptions (comme nous le verrons dans des chapitres ultérieurs, cela est aussi vrai pour les instructions [c scope] et les destructeurs).
Naturellement, de tels blocs de code peuvent aussi lever des exceptions. Les exceptions qui sont levées quand on quitte des blocs de code à cause d'une exception sont appelées [* exceptions collatérales]. L'exception principale et les exceptions collatérales sont des éléments d'une structure de type liste chaînée, dans laquelle toutes les exceptions sont accessibles à travers la propriété [c .next] de l'exception précédente. La valeur de la propriété [c .next] de la dernière exception est [c null] (nous verrons [c null] dans un chapitre ultérieur).
Il y a trois exceptions qui sont levées dans l'exemple suivant~ : L'exception principale qui est levée dans [c foo()] et les deux exceptions collatérales qui sont levées dans les blocs [c finally] de [c foo()] et [c bar()]. Le programme accède aux exceptions collatérales à travers les propriétés~ [c .next].
Certaines concepts qui sont utilisées dans ce programme seront expliqués dans des chapitres ultérieurs. Par exemple, la condition de continuation de la boucle [c for] , [c exc], signifie que [c exc] n'est pas [c null].
[code=d <<<
import std.stdio;
void foo()
{
try {
throw new Exception("Exception levée dans foo");
} finally {
throw new Exception(
"Exception levée dans le bloc finally de foo");
}
}
void bar()
{
try {
foo();
} finally {
throw new Exception(
"Exception levée dans le block finally de bar");
}
}
void main()
{
try {
bar();
} catch (Exception exceptionLevee) {
for (Throwable exc = exceptionLevee;
exc; // ← ce qui veut dire: dans que exc n'est pas 'null'
exc = exc.next) {
writefln("message d'erreur : %s", exc.msg);
writefln("fichier source : %s", exc.file);
writefln("ligne source : %s", exc.line);
writeln();
}
}
}
>>>]
La sortie~ :
[output <<<
message d'erreur : Exception levée in foo
fichier source : deneme.d
ligne source : 6
message d'erreur : Exception levée dans le bloc finally de foo
fichier source : deneme.d
ligne source : 9
message d'erreur : Exception levée dans le bloc finally de bar
fichier source : deneme.d
ligne source : 20
>>>]
[ = Types d'erreurs
Nous avons vu comment le mécanisme des exceptions peut être utile. Il permet aux opérations de bas niveaux comme aux opérations de hauts niveaux d'être interrompues directement, au lieu de laisser le programme continuer avec des données incorrectes ou manquantes ou agir de n'importe quelle autre mauvaise manière.
Ceci ne veut pas dire que chaque état erroné justifie une levée d'exception. On peut parfois faire mieux selon les types d'erreurs.
[ = Erreurs de l'utilisateur
Certaines erreurs sont causées par l'utilisateur. Comme nous l'avons vu précédemment, l'utilisateur peut avoir saisi une chaîne comme «~ bonjour~ » alors que le programme attendait un nombre. Il peut être plus approprié d'afficher un message d'erreur et demander à l'utilisateur de saisir la donnée une nouvelle fois.
Cela dit, il peut être correct d'accepter et d'utiliser l'information directement sans la valider à l'avance, tant que le code qui utilise la donnée lève l'exception quand même. L'important est de pouvoir prévenir l'utilisateur que la donnée n'est pas appropriée.
Par exemple, considérons un programme qui demande un nom de fichier à l'utilisateur. Il y a au moins deux manières de gérer des noms de fichiers potentiellement invalides~ :
- [
Valider l'information avant l'utilisation~ : on peut déterminer si le fichier avec le nom donné existe en appelant la fonction [c exists()] du module [c std.file]~ :
[code=d <<<
if (exists(nomFichier)) {
// oui, le fichier existe
} else {
// non, le fichier n'existe pas
}
>>>]
Cela donne la possibilité de n'ouvrir le fichier que s'il existe. Malheureusement, il est encore possible que le fichier ne puisse pas être ouvert même si [c exists()] retourne [c true], si par exemple un autre processus du système supprime ou renomme le fichier avant que notre programme l'ouvre.
Pour cette raison, la méthode suivante peut être plus utile.
]
- [
Utiliser les données avant de les valider~ : on peut supposer les données valides et les utiliser directement, parce que [c File] lèvera de toute façon une exception si le fichier ne peut pas être ouvert.
[code=d <<<
import std.stdio;
import std.string;
void utiliserLeFichier(string nomFichier)
{
auto fichier = File(nomFichier, "r");
// ...
}
string lireChaîne(in char[] invite)
{
write(invite, ": ");
return chomp(readln());
}
void main()
{
bool fichierUtilisé = false;
while (!fichierUtilisé) {
try {
utiliserLeFichier(
lireChaîne("Entrez un nom de fichier"));
/*
* Si noux arrivons ici, c'est que la fonction
* utiliserLeFichier() s'est déroulée avec succès,
* ce qui indique que le nom de fichier était
* valide.
*
* Nous pouvons maintenant positionner le drapeau
* de boucle afin de quitter la boucle while.
*/
fichierUtilisé = true;
writeln("Le fichier a bien été utilisé");
} catch (std.exception.ErrnoException exc) {
stderr.writeln("Ce fichier n'a pas pu être ouvert");
}
}
}
>>>]
]
[ = Les erreurs du programmeur
Certaines erreurs sont causées par des erreurs du programmeur. Par exemple, le programmeur peut penser qu'une fonction qui vient d'être écrite sera toujours appelée avec une valeur supérieure ou égale à zéro, et cela peut être vrai d'après la conception du programme. Appeler la fonction avec une valeur inférieure ou égale à zéro serait une erreur dans la conception du programme ou dans l'implémentation de cette conception. Ces deux cas peuvent être considérés comme des erreurs de programmation.
Il est plus judicieux d'utiliser [c assert] au lieu du mécanisme des exceptions pour des erreurs qui sont causées par des erreurs du programeur.
[p Note~ : | nous verrons [c assert] dans un chapitre ultérieur.]
[code=d <<<
void gérerSelectionMenu(int selection)
{
assert(selection >= 0);
// ...
}
void main()
{
gérerSelectionMenu(-1);
}
>>>]
Le programme se termine par une erreur d'assertion :
[output <<<
core.exception.AssertError@essai.d(3): Assertion failure
>>>]
[c assert] vérifie l'état du programme et affiche le nom du fichier et le numéro de la ligne de la vérification si elle échoue. Le message précédent indique que l'assertion à la ligne 3 de [c essai.d] a échoué.
]
[ = Situations inattendues
Pour les situations inattendues qui sont hors des deux cas généraux que l'on vient de voir, il est quand même justifié de lever des exceptions. Si le programme ne peut pas continuer son exécution, il n'y a rien d'autre à faire que lever une exception.
Il appartient aux fonctions de plus hauts niveaux qui appellent cette fonction de décider que faire avec les exceptions levées. Elle peuvent attraper les exceptions que nous levons pour rectifier le tir.
]
]
[ = Résumé
- Lorsque que vous êtes confronté à une erreur de l'utilisateur, avertissez l'utilisateur directement ou assurez-vous qu'une exception soit levée~ ; l'exception peut être levée par une autre fonction lors de l'utilisation d'une donnée incorrecte, ou directement par vous.
- Utilisez [c assert] pour vérifier la logique ou l'implémentation du programme (nous verrons [c assert] dans un chapitre ultérieur).
- Utilisez [c enforce] pour valider la bonne utilisation de vos fonctions par les programmeurs.
- En cas de doute, levez une exception.
- Attrapez les exceptions si et seulement si vous pouvez faire quelque chose d'utile avec ces exceptions. Sinon, n'encapsulez pas de code avec une instruction [c try-catch]. Laissez plutôt la gestion de ces exceptions aux couches de code de plus haut niveau qui peuvent en faire quelque chose.
- Ordonnez les blocs [c catch] du plus spécifique au plus général.
- Placez les expressions qui doivent toujours être exécutées lors de la sortie d'une portée dans des blocs [c finally].
]