IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

42. Exceptions

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 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 quatre opérateurs arithmétiques, comme nous l'avons vu dans les exercices d'un chapitre précédent :

 
Sélectionnez
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 switch ci-dessus ne sait que faire avec les opérateurs qui ne sont pas listés dans les instructions case, elle lève donc une exception.

Il y a plein d'exemples d'expressions levées dans Phobos. Par exemple, to!int, qui peut être utilisé pour convertir une représentation textuelle d'un entier à une valeur int lève une exception quand cette représentation n'est pas valide :

 
Sélectionnez
import std.conv;
 
void main()
{
    const int value = to!int("salut");
}

Le programme termine avec une exception levée par to!int :

 
Sélectionnez
std.conv.ConvException@/usr/include/d/4.8/std/conv.d(1826): Unexpected 's' when converting from type string to type int

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 ConvException, qui est défini dans le module std.conv.

42-1. L'instruction throw pour lever des exceptions

Nous avons vu l'instruction throw aussi bien dans les exemples ci-dessus que dans des chapitres précédents.

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 throw ne sont pas exécutées. Ce comportement correspond à la nature des exceptions : elles 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.

42-1-1. Les types d'exceptions Exception et Error

Seuls les types qui sont hérités de la classe Throwable peuvent être levés. 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'Exception ou d'Error, qui sont eux-mêmes des types qui héritent de Throwable. Par exemple, toutes les exceptions que Phobos lève sont héritées de Exception ou Error.

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 Exception. L'héritage est un concept relatif aux classes. Nous verrons les classes dans un chapitre ultérieur.

Les objets de type Exception sont construits à partir d'une valeur string qui représente un message d'erreur. Vous pouvez éventuellement utiliser la fonction format du module std.string pour créer ce message :

 
Sélectionnez
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));
}

Sortie :

 
Sélectionnez
object.Exception...: Nombre de dés invalide : -5

Dans la plupart des cas, au lieu de créer un objet Exception explicitement avec new et lever l'exception avec throw, la fonction enforce() est appelée. Par exemple, l'équivalent de la vérification d'erreur précédente est l'appel suivant à enforce() :

 
Sélectionnez
enforce(nombre  >= 0, format("Nombre de dés invalide : %s", nombre));

Nous verrons les différences entre enforce() et assert() dans un chapitre ultérieur.

42-1-2. Les expressions levées mettent fin à toutes les portées

Nous avons vu que l'exécution du programme commence par la fonction 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, main() peut appeler une fonction nommée faireOmelette(), qui peut à son tour appeler une autre fonction nommée toutPreparer(), qui peut à son tour appeler une autre fonction nommée 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 :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
▶ 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 entrant et sortant sont utilisées pour indiquer les première et dernière lignes des fonctions avec l'aide des caractères ▶ et ◁. Le programme commence par la première ligne de main(), entre dans les autres fonctions et finit par la dernière ligne de main().

Modifions la fonction 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 :

 
Sélectionnez
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 main(). Les parties du programme qui doivent changer sont les suivantes. La valeur invalide de -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 :

 
Sélectionnez
// ...
 
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 :

 
Sélectionnez
▶ 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 preparerOeufs(), toutPreparer(), faireOmelette() et 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 main(). Le chemin que l'exception prend peut être vu comme le chemin rouge dans l'arbre suivant :

Image non disponible

Le but du mécanisme des exceptions est précisément d'avoir ce comportement : sortir de tous les appels de fonctions directement. 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é catch.

42-1-3. Quand utiliser throw

Utilisez 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.

42-2. L'instruction try-catch pour attraper une exception

Comme nous l'avons vu précédemment, une exception levée entraîne la sortie de toutes les fonctions et, finalement, l'arrêt du programme entier.

L'objet exception peut être attrapé avec une instruction try-catch à n'importe quel endroit du chemin qu'elle emprunte pour sortir des fonctions. L'instruction try-catch modélise la phrase « essaie de faire quelque chose et attrape les exceptions qui peuvent être levées ». Voici la syntaxe de try-catch :

 
Sélectionnez
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 try-catch. Le programme lit une valeur d'un dé depuis un fichier et l'affiche sur la sortie standard :

 
Sélectionnez
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 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 le_fichier_qui_contient_la_valeur manque :

 
Sélectionnez
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 ErrnoException est levée et le programme se termine sans afficher « Valeur du dé : ».

Ajoutons une fonction intermédiaire au programme qui appelle lireDeDepuisFichier() depuis un bloc try et main() appellera cette nouvelle fonction :

 
Sélectionnez
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 le_fichier_qui_contient_la_valeur n'existe pas, le programme ne se termine cette fois pas par une exception :

 
Sélectionnez
Impossible de lire depuis le fichier ; on suppose 1
Valeur du dé : 1

Le nouveau programme essaie d'exécuter lireDeDepuisFichier() dans un bloc try. Si ce bloc s'exécute correctement, la fonction se termine normalement avec l'instruction return de;. Si l'exécution du bloc try finit avec l'exception std.exception.ErrnoException, alors le programme entre dans le bloc 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 std.exception.ErrnoException est levée (par File(), pas par notre code) ;
  • cette exception est attrapée par catch ;
  • la valeur de 1 est supposée pendant l'exécution normale du bloc catch ;
  • et le programme continue normalement.

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 try-catch à sa fonction main() :

 
Sélectionnez
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 try contient deux lignes de codes. Toute exception levée depuis une de ces deux lignes sera attrapée par le bloc catch.

Sortie :

 
Sélectionnez
▶ 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 se 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 main().

42-2-1. Les blocs catch sont parcourus séquentiellement

Le type 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.

ConvException et ErrnoException, que nous avons rencontrés plus tô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, ConvException et ErrnoException héritent tous deux de la classe Exception.

Exception et sa sœur Error sont eux-mêmes hérités de 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 Error ou de types qui en héritent. Comme ils sont plus généraux que les types Error, il n'est pas non plus recommandé d'intercepter des objets de type Throwable. Les seuls objets qui devraient normalement être attrapés sont ceux qui font partie de la hiérarchie Exception.

Image non disponible

On verra la représentation hiérarchique plus tard dans le chapitre sur l'héritageHéritage . L'arbre précédent indique que Throwable est la plus générale et qu'Exception et 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 ErrnoException pour gérer une erreur système.

Les exceptions sont attrapées seulement si elles correspondent au type qui est spécifié dans un bloc catch. Par exemple, un bloc catch qui essaie d'attraper une exception SpecialExceptionType n'attrapera pas un ErrnoException.

Le type de l'objet exception qui est levée pendant l'exécution d'un bloc try est comparé aux types qui sont spécifiés par les blocs catch, dans l'ordre dans lequel les blocs catch sont écrits. Si le type de l'objet correspond au type d'un bloc catch, alors l'exception est considérée comme attrapée par ce bloc 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 catch restant sont ignorés.

Du fait que les blocs catch sont testés dans l'ordre, les blocs 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 Exception doit être indiqué au dernier bloc catch.

Par exemple, une instruction try-catch qui essaie d'attraper plusieurs types spécifiques d'exceptions à propos d'une liste d'étudiants peut ordonner les blocs catch du plus spécifique au plus général comme dans le code qui suit :

 
Sélectionnez
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 exception 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
 
}

42-2-2. Le bloc finally

finally est un bloc optionnel de l'instruction try-catch. Il contient des expressions qui doivent être exécutées, qu'une exception ait été levée ou pas.

Pour voir comment finally fonctionne, examinons un programme qui lève une exception 50 % du temps :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 finally est quant à lui toujours exécuté.

42-2-3. Quand utiliser l'instruction try-catch

L'instruction try-catch est utile pour attraper des exceptions pour faire quelque chose de spécial avec.

Pour cette raison, l'instruction 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.

42-3. 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 Exception. Ces propriétés sont fournies par l'interface Throwable :

  • .file : le fichier source à partir duquel l'exception a été levée ;
  • .line : le numéro de la ligne à partir de laquelle l'exception a été levée ;
  • .msg : le message d'erreur ;
  • .info : l'état de la pile du programme quand l'exception a été levée ;
  • .next : l'exception collatérale suivante.

Nous avons vu que les blocs 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 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é .next de l'exception précédente. La valeur de la propriété .next de la dernière exception est null (nous verrons 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 foo() et les deux exceptions collatérales qui sont levées dans les blocs finally de foo() et bar(). Le programme accède aux exceptions collatérales à travers les propriétés .next.

Certaines concepts qui sont utilisés dans ce programme seront expliqués dans des chapitres ultérieurs. Par exemple, la condition de continuation de la boucle for , exc, signifie que exc n'est pas null.

 
Sélectionnez
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 :

 
Sélectionnez
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

42-3-1. Types d'erreurs

Nous avons vu comment le mécanisme des exceptions peut être utile. Il permet aux opérations de bas niveau comme aux opérations de haut niveau 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.

42-3-1-1. 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 exists() du module std.file :

     
    Sélectionnez
    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 exists() retourne 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 File lèvera de toute façon une exception si le fichier ne peut pas être ouvert.
 
Sélectionnez
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 nous 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");
      }
   }
}
42-3-1-1-1. 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 assert au lieu du mécanisme des exceptions pour des erreurs qui sont causées par des erreurs du programmeur.

Note : nous verrons assert dans un chapitre ultérieur.

 
Sélectionnez
void gérerSelectionMenu(int selection)
{
 assert(selection >= 0);
 // ...
}
 
void main()
{
 gérerSelectionMenu(-1);
}

Le programme se termine par une erreur d'assertion :

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

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 essai.d a échoué.

42-3-1-1-2. 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. Elles peuvent attraper les exceptions que nous levons pour rectifier le tir.

42-3-1-2. Résumé
  • Lorsque 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 assert pour vérifier la logique ou l'implémentation du programme (nous verrons assert dans un chapitre ultérieur).
  • Utilisez enforce pour valider la bonne utilisation de vos fonctions par les programmeurs.
  • En cas de doute, levez une exception.
  • Ordonnez les blocs catch du plus spécifique au plus général.
  • 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 try-catch. Laissez plutôt la gestion de ces exceptions aux couches de code de plus haut niveau qui peuvent en faire quelque chose.
  • Placez les expressions qui doivent toujours être exécutées lors de la sortie d'une portée dans des blocs finally.

précédentsommairesuivant