1. Règle : "le plus haut niveau"

La programmation orientée objet (POO) implique un développement à "étage" ou "couche" (notion d'héritage, de sous-éléments...). Un objet peut-être en capsuler dans un autre qui lui-même sera hérité par un objet plus complexe. Une fonction fait souvent appel à d'autres fonctions et ainsi de suite. Dans ce modèle à "étagement" le déclenchement d'une exception, signalant une erreur, peut poser des problèmes de gestion.

A priori, il semblerait logique de gérer l'erreur et de la résorber au niveau local (routine dans laquelle elle est survenue) sans la propager vers les niveaux supérieurs. Mais si l'on y regarde de plus près, cette première approche se révèle vite imparfaite. En effet, si tel est le cas (gestion locale) les couches supérieures n'auront alors aucune information sur la nature et le type d'erreur qui s'est produit plus "bas". Cette erreur silencieuse pour le code appelant peut alors engendrer d'autres exceptions en chaîne. Considérons pour mieux comprendre notre propos une erreur E/S due à l'impossibilité d'ouvrir un fichier (accès refusé). Si le code situé au niveau supérieur n'a pas connaissance de cette erreur, il manipulera un fichier non-ouvert et déclenchera d'autres exceptions de lecture par exemple.

Une question se pose désormais "quel niveau de code peut-être considéré comme étant le plus haut". Je pense que nous pouvons apporter ici un élément de réponse. Le plus haut niveau est celui qui reçoit directement les "instructions" de l'utilisateur. En ce sens certaines procédures événementielles peuvent être considérées comme appartenant au plus haut niveau. Il convient donc, pour ces procédures, de gérer et de résorber définitivement les exceptions. En effet, Il ne faut pas perdre de vue que si l'utilisateur doit être informé d'une erreur, il ne peut en aucun cas la solutionner. C'est pour cette raison qu'il est indispensable d'arrêter à ce stade, la propagation d'une exception.

Il existe sous Delphi plusieurs possibilités de faire remonter l'erreur jusqu'au code appelant.

1-1. Sans gestionnaire d'erreur

C'est la solution la plus facile, le code qui appelle une routine sans gestionnaire hérite en quelque sorte des erreurs que celle-ci peut générer. Cependant cette solution n'offre pas que des avantages. En effet, il faut savoir qu'une exception provoque la sortie de la routine dans laquelle elle survient, si aucun gestionnaire n'est présent. Or il est parfois nécessaire de s'assurer que certains objets ont bien été libérés avant de quitter une procédure ou une fonction.

1-2. Avec un gestionnaire

Deux gestionnaires sont disponibles : Try...Finally et Try...Except.

Try...Finally : Avec ce gestionnaire les instructions placées après Finally constituent un passage obligé. Elles seront exécutées qu'une erreur se produise ou non. Ce gestionnaire est utile quand un code doit être absolument pris en compte même si une exception survient (libération d'une variable de classe...). Try...Finally ne résorbe pas l'erreur elle sera donc communiquée au niveau supérieur.

Exemple :

 
Sélectionnez

Function GetProgramFilesDir() : String;
// Récupère le répertoire des fichiers programme "Program files" 
Var Reg : TRegistry; 
Begin
  Reg := TRegistry.Create;
  Try 
    Reg.RootKey := HKEY_LOCAL_MACHINE; 
    Reg.OpenKey('Software\Microsoft\Windows\CurrentVersion', False);
    Result := Reg.ReadString('ProgramFilesPath');
  Finally 
     Reg.Free; 
  End; 
End;

Commentaire : Quelle que soit l'issue de cette fonction, la variable Reg sera libérée. Si erreur il y a, elle sera ensuite répercutée au niveau de la routine appelant GetProgramFilesDir.

Try...Except : Ici les instructions placées après Except sont exécutées seulement en cas d'erreur. Ce gestionnaire peut servir si un code spécifique doit être pris en compte lorsqu'une exception se produit (retour d'un numéro d'erreur, mise à jour de certains éléments...). Try...Except résorbe l'erreur, le niveau supérieur n'en héritera pas.

Exemple :

 
Sélectionnez

Function LoadFile(Lines : TStrings;FilePath : String) : Bool;
// Gestion du chargement d'un fichier dans une TStrings
Begin 
 If not Assigned(Lines) 
  then Lines:=TStringList.Create;  
 Try 
  TStr.LoadFromFile(FilePath); 
  Result := True; 
 Except 
  ShowMessage(Application.Title + ' ne peut ouvrir : ' + ExtractFileName(FilePath); 
  Result := False; 
 End; 
End;

Commentaire : En cas d'erreur, l'utilisateur sera informé d'un problème (ShowMessage) et la valeur de retour sera mise à False. L'exception ne sera pas héritée par la routine appelant LoadFile. Si tout se passe bien, la fonction retournera True.
La présence d'une valeur de retour nous permettra d'être informé du bon déroulement de la fonction.

Raise permet de déclencher ou de déclencher à nouveau une erreur.

Exemple :

 
Sélectionnez

Procedure LoadFile(Lines : TStrings;FilePath : String);
// Gestion du chargement d'un fichier dans une TStrings
Begin 
 If not Assigned(Lines) 
  then Lines:=TStringList.Create;  
 Try 
  TStr.LoadFromFile(FilePath); 
 Except 
  ShowMessage(Application.Title + ' ne peut ouvrir : ' + ExtractFileName(FilePath); 
  Raise; 
 End; 
End; 

Commentaire : Dans la version précédente de cette routine, l'erreur était signalée par la valeur de retour de la fonction avec comme inconvénient la perte du type et de la nature de l'exception. Pour récupérer l'erreur elle-même au niveau du code appelant nous avons placé ici l'instruction Raise après Except.

* Pour plus de détail sur ces gestionnaires reportez-vous à la documentation Delphi.

2. Règle : "la continuité du code"

Quand une exception survient, nous pouvons nous poser la question suivante : " Faut-il laisser l'erreur provoquer la sortie du code ou continuer certaines "tâches" pour mener à bien la suite des opérations prévues. Ceci dépend bien entendu de la nature et du type d'erreur. Mais nous pouvons dès à présent donné un début de réponse à cette question. Si l'erreur, quand elle survient, a des chances de déclencher à nouveau une série d'exceptions en chaîne, il vaut mieux sortir au plus vite et signaler l'échec. Prenons par exemple deux opérations OuvrirFichier - LireFichier. Si OuvrirFichier provoque une erreur alors ce n'est pas la peine de continuer en forçant le code à passer par LireFichier. En outre, si l'erreur est bien ciblée et que, tout risque d'en provoquer d'autre, est écarté alors nous pouvons envisager de relancer le processus en se branchant après l'instruction qui à provoquer l'échec.

Sous DELPHI cette problématique peut être solutionnée de plusieurs manières, sachant que chaque situation requiert une analyse adaptée:

3. Imbriquer des gestionnaires d'erreurs

- Calcul d'une vitesse et interprétation de celle-ci -

 
Sélectionnez

Procedure Vitesse;
Var T, 
    D, 
	Vms, 
	Vkh  : Real48;

    Msg  : String;
    
Begin
  Try
   D := StrToInt(InputBox('Entrez la distance en mètre', 'Distance', '0'));
   T := StrToInt(InputBox('Entrez le temps en seconde', 'Temps', '0'));
   Try
    Vms := D / T;
   Except
    Vms := 0;
   End;
   Vkh:=(Vms * 3600) / 1000;  
   
   Case Vkh of
     0..100   : Msg:='Attention à ne pas dépasser 100 km/h';
     101..150 : Msg:='Vous roulez dangereusement';
    Else
      Msg:='Vous êtes inconscient !!'; 
   End;
   Showmessage('Votre vitesse est de + FloatToStr(Vkh) + ' km/h : ' + Msg);
  Except 
   ShowMessage('Valeur incorrecte');
  End;
End; 

Commentaire : Le premier gestionnaire s'occupera des erreurs de saisie (caractères au lieu de chiffes, nombres trop grand) et avertira l'utilisateur le cas échéant "Except ShowMessage('Valeur incorrecte');". Le second gestionnaire quant à lui, se chargera des erreurs de calcul de la ligne "Vms := D / T;" (division par zéro). Si exception il y a, la vitesse sera alors réinitialisée à 0 "Except Vms:=0;". En effet la saisie de 0 pour le temps, est incohérente d'un point de vue du calcul, mais peut très bien être un choix volontaire ou non de l'utilisateur. L'imbrication des gestionnaires permet ainsi de calculer et de donner une interprétation de la vitesse par l'instruction "Case Vkh Of..." même si une erreur est déclenchée à la ligne "Vms := D / T;". Autrement dit, sans la présence imbriquée du second gestionnaire, l'exception EZeroDivide aurait provoqué la sortie du code et aucune appréciation de la vitesse n'aurait été possible.

4. Gestionnaire à l'intérieur d'une boucle

- Changement d'attributs d'une liste de fichiers -

 
Sélectionnez

Procedure ChangeAttrFichiers (ListeDeFichiers : TString);
Var
  I : Integer;
Begin
  For I:=0 To ListeDeFichiers.Count - 1 Do
   Try
    FileSetAttr(ListeDeFichiers.Items[I],[faArchive]);
   Except
    ShowMessage('Impossible de changer l''attribut pour le fichier : ' + ListeDeFichiers.Items[I]);'
   End;
End; 

Commentaire : Si une erreur se produit lors du changement d'attribut d'un fichier (accès refusé par exemple) alors le gestionnaire signale l'impossibilité rencontrée :

 
Sélectionnez

ShowMessage('Impossible de changer l''attribut pour le fichier : ' + ListeDeFichiers.Items[I]);'

Pour autant la boucle ne sera pas interrompue et les fichiers suivants seront traités.

5. Règle : "prévenir vaut mieux que guérir"

Il est souvent préférable d'anticiper une erreur avant de la récupérer dans un gestionnaire, de cette façon nous réduisons le nombre et le type d'erreurs à gérer. L'idée ici est de prévenir le risque d'exception en simulant les divers trajets de code possibles. Malheureusement il n'existe pas de recette miracle, seule l'expérience du développeur et sa capacité de d'anticipation ferons la différence. Néanmoins plusieurs fonctions et instructions DELPHI permettent de faciliter ce travail en vérifiant la validité et la pertinence d'un objet (FileExists, Assigned, Nil...). Nous allons en illustré deux. Reprenons la procédure exposée plus haut et ajoutons-y deux lignes :

 
Sélectionnez

Procedure ChangeAttrFichiers (ListeDeFichiers : TString);
Var
  I : Integer;
Begin
  If Not Assigned(ListeDeFichiers) 
   then Exit; // première ligne insérée
   
  For I:=0 To ListeDeFichiers.Count - 1 Do
   Try
    If  FileExists(ListeDeFichiers.Items[I]) 
      // deuxième ligne insérée
	 then FileSetAttr(ListeDeFichiers.Items[I],[faArchive]);
   Except
    ShowMessage('Le fichier : ' + ListeDeFichiers.Items[I] + ' est inaccessible ou est endommagé.');'
   End;
End; 

Commentaire : La première ligne rajoutée nous permettra d'éviter une exception si la liste des fichiers n'a pas été initialisée (pointeur de liste invalide). Nous anticiperons ainsi une erreur de violation d'accès. La deuxième ligne quant à elle testera l'existence du fichier avant de modifier ses attributs. Le gestionnaire d'erreurs Try...Except se cantonnera donc à récupérer les exceptions provoquées par un accès refusé ou un fichier endommagé. De cette manière nous pouvons mieux cibler le message :

 
Sélectionnez

ShowMessage('Le fichier : ' + ListeDeFichiers.Items[I] +  est inaccessible ou est endommagé.');'

Pour conclure nous pouvons dire que ces trois règles sont données à titre indicatif. Rien ne permet de dire qu'elles ont une valeur universelle, car la gestion des erreurs est bien-entendu liée au contexte et au besoin de telle ou telle application. Pour compléter cette approche je vous invite à prendre connaissance (aide en ligne DELPHI) des classes d'exceptions (création, gestion...) et de la possibilité d'identifier ces classes grâce à l'instruction On...Do.

Alexandre le Grand (Mars 2002).