I. 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 encapsulé 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 produite 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.
I-A. Sans gestionnaire d'erreurs▲
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.
I-B. 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 :
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 :
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 :
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étails sur ces gestionnaires reportez-vous à la documentation Delphi.
II. 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'erreurs. Mais nous pouvons dès à présent donner 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'autres, 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 résolue de plusieurs manières, sachant que chaque situation requiert une analyse adaptée.
III. Imbriquer des gestionnaires d'erreurs▲
- Calcul d'une vitesse et interprétation de celle-ci -
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 grands) 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.
IV. Gestionnaire à l'intérieur d'une boucle▲
- Changement d'attributs d'une liste de fichiers -
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 :
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.
V. 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, seules l'expérience du développeur et sa capacité d'anticipation feront 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 illustrer deux. Reprenons la procédure exposée plus haut et ajoutons-y deux lignes :
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 :
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).