Guide Pascal et Delphi

Date de publication : 10/04/2000 , Date de mise à jour : 01/04/2008


XIX. Gestion des exceptions
XIX-A. Introduction
XIX-B. Try..Finally
XIX-C. Try..Except
XIX-C-1. Grammaire
XIX-C-2. Listes non exhaustive de classe d'exception
XIX-D. Exception personnalisée
XIX-E. Raise
XIX-F. Conclusion


XIX. Gestion des exceptions

Le présent tutoriel n'a pas la prétention de tout vous apprendre sur la gestion des exceptions (comme tous les tutoriaux que je fais mais celui-la plus que les autres). Mais vous devriez vous sentir à l'aise avec la bête et ne plus rechigner à les utiliser. Pour ceux qui ont déjà pris l'habitude d'utiliser leurs services, je leur conseille de le parcourir quand même, il se pourrait qu'ils apprennent quelque chose. Si tel n'était pas le cas, toutes mes excuses (et mieux encore vous en connaissez plus sur le sujet, je vous invite à compléter/modifer ce tutorial).


XIX-A. Introduction

Tout d'abord qu'est qu'une exception ? Comme son nom l'indique une exception est un événement imprévu par le programmeur (ou programme). Vous me direz comment peut-il gérer quelques chose qu'il n'a pas prévu ? Très simple, le programmeur sachant que le monde n'est pas parfait aura pris le soin de protéger des blocs d'instructions sensibles.

Concrètement, lorsque vous développez votre application, vous vous attendez à ce que telle ou telle chose soit disponible (un fichier dll par exemple) ou que l'utilisateur entre tel type de donnée dans telle variable (champ de saisie où il faut entrer des nombres) (par utilisateur, je considère une personne ou le programme lui-même) or il arrive que les choses ne se passent pas tout à fait comme vous les avez souhaitées, fichier absent, caractère vers une variable numérique etc...
Pour protéger votre application et plus précisément un bloc de code, une solution est d'utiliser les exceptions et ainsi éviter à votre application de planter lamentablement. Une autre solution serait de faire des tests mais si pour des cas simples, les tests sont faciles à mettre en place (l'exemple de la division par zéro est assez parlant), il n'en est pas de même pour des tâches plus complexes ou sujettes à différents types d'erreur (comme l'accès à un fichier). D'autres diront que gérer par exception, les cas à problème, allège le code (à vous de voir).

Les exceptions peuvent être de natures différentes suivant l'opération où elles se produisent. Ces différentes exceptions sont appelées type d'exception ou classe d'exception. Par exemple lors d'une tentative de conversion d'un string en float, si le string contient des lettres, on aura une exception de classe conversion (EConverError).

La protection d'un bloc de code se fait par l'utilisation d'un couple de mot clé. Suivant la façon dont l'on veut gérer l'exception, deux couples existent, try..finally et try..except. Les deux couples protégent le code situé entre try et l'autre mot clé, si une exception arrive entre ces deux mots clés , le programme arrête de traiter le bloc protégé et passe tout de suite aux instructions comprises entre le second mot clé du couple et poursuit ensuite les instructions suivant ce second bloc de code. La différence entre le couple try..finally et try..except se situe dans l'exécution du second bloc d'instruction.

  • Avec try..finally, les instructions du second bloc d'instruction sont toujours exécutées.
    
    instructions1
    try
       // Bloc de code à protéger
       instructions protégées
    finally
       // Second bloc de code
       // Ce bloc sera exécuté à la fin des instructions protégées
       // ou dès qu'une erreur survient dans le bloc de code protégé
       instructions2
    end;
    // suite des instructions qui seront exécutées si elles ne provoquent pas d'erreur
    instructions3
    
  • Avec try..except, les instructions du second bloc d'instruction ne sont exécutées que s'il y a eut une exception dans le bloc protégé. Le couple try..except propose en plus de traiter, l'exception en fonction de la classe d'exception.
    
    instructions1
    try
       // Bloc de code à protéger
       instructions protégées
    except
       // Second bloc de code
       // Ce bloc ne sera exécuté que si une erreur survient dans la partie protégée
       instructions2
    end;
    // suite des instructions qui seront exécutées si elles ne provoquent pas d'erreur
    instructions3
    
Vous l'aurez remarqué, le bloc dit protégé est celui qui le paraît le moins. En fait par bloc protégé, je pense que l'on désigne la protection du programme contre un code sensible. Votre programme ne va plus planter lamentablement lorsqu'il rencontrera une erreur dans un bloc protégé. Par contre vous continuerez d'avoir les messages d'erreur en l'exécutant depuis Delphi, cela est destiné à vous aider à vérifier que vous interceptez bien la bonne classe d'exception.


XIX-B. Try..Finally

La syntaxe de try..finally est :

try
   instruction1
finally
   instruction2
end;
Instruction1 est une partie sensible du code (manipulation d'un fichier, création d'un objet, etc...) et instruction2 une partie du code qui doit être exécuté quoiqu'il arrive (libération de ressource, fermeture d'une connexion etc...). Si une erreur survient dans les instructions du bloc instruction1, l'exécution passe immédiatement à l'exécution d'instruction2 sinon l'exécution termine les instructions et passe ensuite au instruction2.
Vous l'aurez compris, son utilisation est fortement recommandée pour libérer une ressource même si le programme rencontre une erreur. Mais attention, l'instruction demandant la ressource doit se trouver à l'extérieur du try..finally. Dans le cas contraire, s'il arrivait que le programme ne puisse pas allouer la ressource, tenter de la libérer peut provoquer une erreur.

Exemple : Création d'un objet

var
   Ob : TObjetExemple;
begin
   Ob := TObjetExemple.Create; // Création de l'objet
   try
      {instruction}
   finally
      Ob.Free; // Libération
   end;
end; 
Exemple : assignation d'un fichier

function OuvrirF(Nom : TFileName) : boolean;
var
   F : Textfile;
   S : string;
   i, j, valeur :integer;
begin
   AssignFile(F,Nom);
   try
      Reset(F);
      readln(F,S);

      {instruction}

      Result := True;
   finally
      CloseFile(F);
   end;
end; 
Notez la position de demande d'allocation de ressource par rapport au try..finally.

idea Attention :

Tant que vous n'avez pas appelé CloseFile(F), vous ne pouvez pas manipuler le fichier (renommer, détruire, déplacer etc...). Ne l'oubliez pas ! Ceci est valable pour les fichiers mais aussi pour d'autres ressources (base de donnée, périphérique...).
idea Précision :

La protection d'un bloc de code permet d'éviter la propagation du message d'erreur mais dans certain cas, il peut être nécessaire de relancer sa diffusion. La commande raise peut être utilisée à cet effet voir son chapitre pour plus de précision.

XIX-C. Try..Except


XIX-C-1. Grammaire

Examinons une application fournissant un champ de saisie n'attendant que des nombres comme saisie. Deux solutions s'offrent à vous, la première empêcher l'utilisateur d'entrer autre chose que des caractères numériques (assez compliqué à mettre en place mais mieux au niveau de l'ergonomie) et la seconde utiliser les exceptions. Laissons la première de côté et intéressons-nous à la seconde. Ce que nous devons protéger est le moment où la saisie de l'utilisateur doit être affectée à une variable de type numérique. Vous pouvez tester en réalisant une application faisant une telle opération. Lors de l'exécution, un message d'erreur se produira dès que vous affecterez des lettres à la variables, plantant le plus souvent votre application. Par contre en protègeant votre bloc de code, non seulement, vous limitez l'erreur à cette portion de code et vous pouvez en plus réaliser un traitement spécifique au problème (réinitialiser des variables, informer l'utilisateur...).

La syntaxe est la suivante :

try
   instruction1
except
   instruction2
end;
instruction3
Instruction1 est comme pour le try..finally la partie sensible du code tandis qu'instruction2 le code qui sera exécuté si instruction1 provoque une erreur. Si une erreur survient dans les instructions du bloc instruction1, l'exécution passe immédiatement à l'exécution d'instruction2 sinon l'exécution termine les instructions et passe ensuite au instruction3.

Exemple : Gestion des erreurs liées à une conversion de Type.

procedure TForm1.Button1Click(Sender: TObject);
var
   param1 : Double;
begin
   try
      param1 := StrToFloat(Edit1.Text);

      {suite des instructions}

   except
      on EconvertError do
         MessageDlg('Erreur : Vous devez entrer un réel'
            +#10#13+'Le séparateur décimal est : '+DecimalSeparator, mtError, [mbOk], 0);
   end;
   {Autre instruction non sensible}
end;
Essayez cet exemple, en cas d'erreur de saisie vous aurez droit à un message d'erreur un peu plus clair que ceux distillés par Windows. Pour le vérifier, tapez votre chiffre en vous trompant dans le séparateur décimal (le point au lieu de la virgule et vice versa), Sans la gestion d'erreur vous saurez seulement que votre saisie n'est pas valide sans comprendre pourquoi car vous avez bien entré un nombre alors que grâce à la gestion des exceptions, vous aurez le droit à :

En plus, vous pouvez ajouter des instructions remettant votre programme dans un état stable (réinitialisation de variable par exemple).

L'exemple ci dessous est une des façons d'écrire la gestion des exceptions par try..except. Dans ce cas précis, nous savions ce qui pouvait provoquer une erreur dans le code protégé (une erreur de conversion) et nous n'avons traité que ce cas.
D'une manière plus générale, on peut considérer que la gestion d'exception peut intercepter des erreurs prévisibles et d'autre plus aléatoires (non prévues) et que l'on peut soit traiter les erreurs prévisibles soit les autres ou les deux (ce qui quand même préférable).
Quand on veut traiter une erreur prévisible, il faut savoir à quelle classe elle appartient, par exemple une erreur de conversion appartient à la classe EConvertError (on peut savoir ceci en consultant dans l'aide, le type d'erreur soulevée par une fonction particulière).

Le try..except pourra se présenter ainsi :

try
   {instructions}
except
    {instruction éventuelle commun à tous les cas possibles d'erreur}

    // Gestion des cas prévisibles d'erreur
    on Exception1 do InstructionTraitantErr1;
    on Exception2 do InstructionTraitantErr2;
    ....
    on Exception(n) do InstructionTraitantErr(n);
else
    InstructionTraitantLesCasNonPrevue;
end; 

XIX-C-2. Listes non exhaustive de classe d'exception

Ceci est une liste incomplète des classes d'exception que vous pouvez être amené à utiliser.

  • EconvertError : Erreur de conversion, vous essayez de convertir un type en un autre alors qu'ils sont incompatibles.
    Exemple : un string en integer (StrToInt) avec un string ne correspondant pas à un nombre entier.
  • EDivByZero : Le programme a tenté de faire une division par zéro (pas Cool).
  • EFOpenError : Le programme ne peut ouvrir un fichier spécifié (le fichier n'existe pas par exemple)
  • EInOutError : Erreur d'entrée-sortie, sur le fichier spécifié
  • EReadError : le programme tente de lire des données dans un flux mais ne peut lire le nombre spécifié d'octets.
  • ERangeError : Débordement de taille. Le programme dépasse les borne d'un type entier ou les limites d'un tableau.
  • EAbort : Exception spéciale car elle n'affiche pas de message d'erreur. On peut s'en servir pour annuler une tache en cours si une condition arrive (une erreur par exemple). Pour la déclencher un simple appel à Abort; suffit, une exception EAbort est alors générée. C'est un moyen simple de créer une exception personnalisée, il suffit alors de traiter l'exception on EAbort do.
Si vous n'arrivez pas à trouver la classe d'exception correspondant à votre code, tenter de provoquer l'erreur. Dans le message d'erreur, Delphi vous indiquera la classe d'exception (si elle existe), ce message permet aussi de tester si on a intercepté la bonne classe d'exception.


XIX-D. Exception personnalisée

Toutes les exceptions dérivent de la Class exception, vous pouvez donc créer vos propres classes d'exception en héritant de cette classe. Ceci peut vous permettre de gérer votre programme par exception en supprimant tous les test des cas qui ne vous intéresse pas (exemple : la vérification qu'un diviseur est non nul).

En disant que toutes les exceptions dérivent de la classe Exception, je ne suis pas tout à fait exact. En fait n'importe quel objet peut être déclenché en tant qu'exception. Cependant, les gestionnaires d'exception standard ne gèrent que les exceptions dérivant de la classe exception.

Dans votre programme, si vous voulez déclarer une nouvelle classe d'exception, vous aurez à entrer le code suivant :

type
  MonException = class(Exception)
    [HelpContext : THelpContext; // Context dans l'aide]
    [Message : string; // Message d'erreur]
  public
    FonctionGerantErreur(); // Fonction à appeler en cas d'erreur
  end;

FonctionGerantErreur();
begin
  {instructions}
end;
Seule la première ligne est obligatoire. Si vous ne précisez pas le reste, la seule information disponible lors du déclenchement de votre exception sera son nom. FonctionGerantErreur est le nouveau gestionnaire de l'exception, dans cette fonction vous mettrez le code assurant la stabilité de votre application.

idea Remarque :

Pour accéder au message ou à la Méthode d'une Exception (FonctionGerantErreur) tapez E.Message ou E.MaFonction. Dans ce cas le try..except doit s'écrire ainsi :

try
  instruction
except
  on E : Exception do
    ShowMessage('Message : ' + E.Message);
    // Et/Ou
    E.MaFonction; // Permet de Centraliser le code gérant un type d'erreur
end; 
Exemple : Exception personnalisée

type
   EValeurIncorrect = class(Exception);
et dans le code

if Valeur <> ValeurCorrect then
    raise EValeurIncorrect.Create('Valeur ne fait pas partie des valeurs autorisées');
La propriété Message de la classe Exception (y compris les classes dérivées) et l'affichage de message personnalisé dans le bloc except/end sont équivalents. Pour les classes d'exception déjà existantes, on préférera sans doute rendre le message plus explicite tandis que pour les classes personnalisées on aura recours à la propriété Message plutôt que d'indiquer à chaque fois le texte.
Il en est de même pour les instructions gérant l'erreur. On n'utilisera la Méthode de la classe que pour ceux personnalisées, évitant d'utiliser une fonction orpheline ou pire de réécrire à chaque fois le code.

Reportez-vous sur les classes pour plus de renseignement sur l'héritage.


XIX-E. Raise

Protéger ainsi votre code, vous permet d'intercepter les messages d'erreur. Toutefois dans certaine situation, vous souhaiterez que le message d'erreur soit propagé pour qu'il soit intercepté par une autre gestion des exceptions.
Prenons l'exemple de l'assignation de fichier, plutôt que d'utiliser une variable de retour pour indiquer le résultat de l'opération, on pourrait transmettre le message d'erreur éventuel. A cet effet, la commande raise est à votre disposition.
Raise ne sert pas uniquement à propager un message d'erreur, on peut aussi s'en servir pour déclencher une exception (en générale pour déclencher une exception personnalisée).

Ci-dessous, un exemple de déclenchement d'exception personnalisée.

if Valeur <> ValeurCorrect then
    raise EValeurIncorrect.Create('Valeur ne fait pas partie des valeurs autorisées');
Exemple complet de l'utilisation de raise :

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TForm1 = class(TForm)
    Edit1: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Déclarations privées }
  public
    { Déclarations publiques }
  end;

  MonException = class(Exception)
  public
     function GestErr():string;
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

function MonException.GestErr():string;
begin
   if MessageDlg('La variable transmise est incorrect continuer avec la valeur par défaut', mtInformation, [mbYes, mbNo], 0) =
       mrYes then
   begin
      Result := 'très petit';
   end;
end;

function DoQuelqueChose(valeur : double):string;
begin
   // La fonction ne travaille que sur des nombres réels positifs
   if valeur < 0 then
   begin
      raise MonException.Create('Erreur: Travaille impossible !');
   end;

   // Travaille sur valeur complètement sans intérêt
   if valeur < 10 then
      result := 'très petit'
   else if valeur <= 50 then
      result := 'moitié de cent'
   else if valeur <= 100 then
      result := 'égal à cent'
   else
      result := 'très grand';
end;

procedure TForm1.Button1Click(Sender: TObject);
var
   r : double;
   tmpstr : string;
begin
   try
      r := StrToFloat(Edit1.Text);
      tmpstr := DoQuelqueChose(r);
   except
    on E : MonException do
      tmpstr := E.GestErr;
    on EconvertError do
    begin
      ShowMessage('Erreur de Saisie : nombre attendu'
         +#10#13+'Séparteur décimal : '+DecimalSeparator);
      tmpstr := 'Mauvaise saisie';
    end;
    else
    begin
       ShowMessage('Erreur Inconnue');
       tmpstr := 'Invalid';
    end;
   end;
   ShowMessage('Résultat : '+tmpStr);
end;

end.

XIX-F. Conclusion

Grâce aux exceptions apparues avec la programmation objet, le développeur a maintenant à sa disposition un outil efficace pour protéger son programme des aléas de l'informatique. J'espère que le présent tutorial a été pour vous une mine d'information et que désormais vous aborderez la gestion des exceptions avec sérénité.

 
  

Copyright © 2008 Frédéric Beaulieu. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.