Gestion des Exceptions Delphi


A propos des exceptions dans DELPHI


Une exception est déclenchée quand une erreur ou un autre événement interrompt le déroulement normal d'un programme. L'exception transfère le contrôle à un gestionnaire d'exception, ce qui vous permet de séparer la logique normale d'exécution du programme de la gestion des erreurs. Comme les exceptions sont des objets, elles peuvent être regroupées en hiérarchies en utilisant l'héritage et de nouvelles exceptions peuvent être ajoutées sans affecter le code existant. Une exception peut véhiculer des informations, par exemple un message d'erreur, depuis le point où elle est déclenchée jusqu'au point où elle est gérée. 
Quand une application utilise l'unité SysUtils, la majorité des erreurs d'exécution sont automatiquement converties en exceptions. Nombre d'erreurs qui, autrement, provoqueraient l'arrêt d'une application (mémoire insuffisante, division par zéro, erreurs de protection générales) peuvent ainsi être interceptées et gérées.

Quand utiliser des exceptions

Les exceptions offrent un moyen élégant d'intercepter les erreurs d'exécution sans arrêter le programme et sans utiliser d'encombrantes instructions conditionnelles. Les exigences imposées par la sémantique de la gestion des exceptions se traduisent par une pénalisation au niveau de la taille du code ou des données et au niveau des performances à l'exécution. Il est possible de déclencher des exceptions pour presque toutes les raisons et de protéger pratiquement n'importe quel bloc de code en l'intégrant dans une instruction try...except ou try...finally, mais, en pratique, il vaut mieux réserver ces outils à des situations particulières. 

La gestion des exceptions convient aux erreurs qui ont peu de chances de se produire, mais dont les conséquences sont quasiment catastrophiques (le crash d'une application, par exemple) ; aux conditions d'erreurs difficiles à tester dans des instructions if...then ; et quand vous avez besoin de répondre aux exceptions déclenchées par le système d'exploitation ou par des routines dont le code source n'est pas sous votre contrôle. Les exceptions sont couramment utilisées pour les erreurs matérielles, de mémoire, d'entrée/sortie et du système d'exploitation. 
Les instructions conditionnelles sont souvent le meilleur moyen de tester les erreurs. Par exemple, supposons que vous vouliez vous assurer de l'existence d'un fichier avant d'essayer de l'ouvrir. Vous pourriez le faire comme ceci :

try
    AssignFile(F, FileName);
    Reset(F);     // déclenche une exception EInOutError si le fichier est introuvable
except
    on Exception do ...
end;

Mais, vous pourriez aussi éviter la lourdeur de gestion d'exception en utilisant :

if FileExists(FileName) then    // renvoie False si le fichier est introuvable ; ne déclenche aucune exception

begin
    AssignFile(F, FileName);
    Reset(F);
end;

Les assertions fournissent un autre moyen de tester une condition booléenne à n'importe quel endroit du code. Quand une instruction Assert échoue, le programme s'arrête avec une erreur d'exécution ou (s'il utilise l'unité SysUtils) déclenche une exception EAssertionFailed. Les assertions devraient n'être utilisées que pour tester les conditions que vous ne souhaitez pas voir se produire.

Déclaration des types exception

Les types exception sont déclarés comme les autres classes. En fait, il est possible d'utiliser comme exception une instance de toute classe. Il est néanmoins préférable de dériver les exceptions de la classe Exception définie dans SysUtils. 
Vous pouvez grouper les exceptions en familles en utilisant l'héritage. Par exemple, les déclarations suivantes de SysUtils définissent une famille de types exception pour les erreurs mathématiques :

type
   EMathError = class(Exception);
   EInvalidOp = class(EMathError);
   EZeroDivide = class(EMathError); 
   EOverflow = class(EMathError);
   EUnderflow = class(EMathError);

Etant donné ces déclarations, vous pouvez définir un seul gestionnaire d'exceptions EMathError qui gère également EInvalidOp, EZeroDivide, EOverflow et EUnderflow. 
Les classes d'exceptions définissent parfois des champs, des méthodes ou des propriétés qui contiennent des informations supplémentaires sur l'erreur. Par exemple,

type EInOutError = class(Exception) 
       ErrorCode: Integer;
     end;

Déclenchement et gestion des exceptions

Pour déclencher un objet exception, utilisez une instance de la classe d'exception avec une instruction raise. Par exemple,

raise EMathError.Create;

En général, une instruction raise a la forme : Raise objet at adresse 

Où "objet" et "at adresse" sont tous deux facultatifs. Lorsqu'une adresse est spécifiée, cela peut être n'importe quelle expression dont le résultat est un type pointeur, mais c'est habituellement un pointeur sur une procédure ou une fonction. Par exemple :

raise Exception.Create('Paramètre manquant') at @MyFunction;

Utilisez cette option pour déclencher l'exception depuis un emplacement de la pile antérieur à celui où l'erreur s'est effectivement produite. 
Quand une exception est déclenchée (c'est-à-dire qu'elle est référencée dans une instruction raise), elle est régie par la logique particulière de gestion des exceptions. Une instruction raise ne renvoie jamais le contrôle d'une manière normale. Elle transfère à la place le contrôle au gestionnaire d'exception le plus proche capable de gérer les exceptions de la classe donnée. Le gestionnaire le plus proche correspond au dernier bloc try...except dans lequel le flux d'exécution est entré sans en être encore sorti. 
Par exemple, la fonction suivante convertit une chaîne en entier et déclenche une exception ERangeError si la valeur résultante est hors de l'intervalle spécifié :

function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
    Result := StrToInt(S);  // StrToInt est déclarée dans SysUtils
    if (Result < Min) or (Result > Max) then
       raise ERangeError.CreateFmt('%d n''est pas dans l''intervalle spécifié %d..%d', [Result, Min, Max]);
end;

Remarquez la méthode CreateFmt appelée dans l'instruction raise. Exception et ses descendants ont des constructeurs spéciaux qui proposent d'autres moyens de créer des messages d'exception et des identificateurs de contexte. 
Une exception déclenchée est automatiquement détruite une fois qu'elle a été gérée. N'essayez jamais de détruire manuellement une exception déclenchée.

Remarque: Le déclenchement d'une exception dans la section initialisation d'une unité peut ne pas donner le résultat attendu. La gestion normale des exceptions provient de l'unité SysUtils qui doit d'abord être initialisée pour que cette gestion soit utilisable. Si une exception se produit lors de l'initialisation, toutes les unités initialisées (dont SysUtils) sont finalisées et l'exception est redéclenchée. Alors, l'exception est interceptée et gérée, généralement par l'interruption du programme. De la même façon, le déclenchement d'une exception dans la section finalisation d'une unité risque de ne pas produire le résultat escompté si SysUtils a déjà été finalisé lorsque l'exception a été déclenchée.

Instructions Try...except

Les exceptions sont gérées dans des instructions try...except. Par exemple,

try
   X := Y/Z;
   except
     on EZeroDivide do HandleZeroDivide;
end;

Cette instruction tente de diviser Y par Z mais appelle la routine HandleZeroDivide si une exception EZeroDivide est déclenchée. 
L'instruction try...except a la syntaxe suivante : 
try instructions except exceptionBlock end 
où instructions est une suite d'instructions (délimitée par des points-virgule) et exceptionBlock est soit
  • une autre suite d'instructions, soit
  • une suite de gestionnaires d'exceptions, éventuellement suivie par
des instructions else 

Un gestionnaire d'exception a la forme : On identificateur: type do instruction 

identificateur: est facultatif (si identificateur est précisé, ce doit être un identificateur valide), type est le type utilisé pour représenter les exceptions et instruction est une instruction quelconque. 
Une instruction try...except exécute les instructions dans la liste initiale instructions. Si aucune exception n'est déclenchée, le bloc exception (exceptionBlock) n'est pas pris en compte et le contrôle passe à l'instruction suivante du programme. 
Si une exception est déclenchée lors de l'exécution de la liste instructions initiale, que ce soit par une instruction raise dans la liste instructions ou par une procédure ou une fonction appelée dans la liste instructions, il va y avoir une tentative de "gestion" de l'exception :
  • Si un des gestionnaires du bloc exception ne correspond à l'exception, le contrôle passe au premier d'entre eux. Un gestionnaire d'exceptions "correspond" à une exception si le type du gestionnaire est la classe de l'exception ou un ancêtre de cette classe.
  • Si aucun gestionnaire correspondant n'est trouvé, le contrôle passe à l'instruction de la clause else si elle est définie.
  • Si le bloc d'exception est simplement une suite d'instructions sans gestionnaire d'exception, le contrôle passe à la première instruction de la liste.
Si aucune de ces conditions n'est respectée, la recherche continue dans le bloc exception de l'avant-dernière instruction try...except dans laquelle le flux du programme est entré et n'est pas encore sorti. S'il n'y a là aucun gestionnaire approprié, aucune clause else, ni aucune liste d'instructions, la recherche se propage à l'instruction try...except précédente, et ainsi de suite. Si l'instruction try...except la plus externe est atteinte et que l'exception n'est toujours pas gérée, le programme s'interrompt. 

Quand l'exception est gérée, le pointeur de la pile est ramené en arrière jusqu'à la procédure ou la fonction contenant l'instruction try...except où la gestion a lieu et le contrôle d'exécution passe au gestionnaire d'exception exécuté, à la clause else ou à la liste d'instructions. Ce processus efface tous les appels de procédure ou de fonction effectués à partir de l'entrée dans l'instruction try...except où l'exception est gérée. L'objet exception est alors automatiquement détruit par un appel de son destructeur Destroy et le contrôle revient à l'instruction suivant l'instruction try...except. Si un appel d'une procédure standard Exit, Break ou Continue force la sortie du gestionnaire d'exception, l'objet exception est quand même détruit automatiquement. 

Dans l'exemple suivant, le premier gestionnaire d'exception gère les exceptions division-par-zéro, le second gère les exceptions de débordement et le dernier gère toutes les autres exceptions mathématiques. EMathError apparaît en dernier dans le bloc exception car c'est l'ancêtre des deux autres classes d'exception ; s'il apparaît en premier, les deux autres gestionnaires ne sont jamais utilisés.
try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
end;
Un gestionnaire d'exception peut spécifier un identificateur avant le nom de la classe exception. Cela déclare l'identificateur représentant l'objet exception pendant l'exécution de l'instruction suivant on...do. La portée de l'identificateur est limitée à celle de l'instruction. Par exemple,

try
  ...
except
  on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;

Si le bloc exception spécifie une clause else, la clause else gère toutes les exceptions qui ne sont pas gérées par les gestionnaires du bloc. Par exemple,

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
else
  HandleAllOthers;
end;

Ici, la clause else gère toutes les exceptions qui ne sont pas des erreurs mathématiques (EMathError). 
Si le bloc exception ne contient pas de gestionnaires d'exceptions mais une liste d'instructions, cette liste gère toutes les exceptions. Par exemple,

try
   ...
except
   HandleException;
end;

Ici la routine HandleException gère toutes les exceptions se produisant lors de l'exécution des instructions comprises entre try et except.
Redéclenchement d'exceptions

Quand le mot réservé raise apparaît dans un bloc exception sans être suivi d'une référence d'objet, il déclenche l'exception qui était gérée par le bloc. Cela permet à un gestionnaire d'exception de répondre à une erreur d'une manière partielle, puis de redéclencher l'exception. Cela est pratique quand une procédure ou une fonction doit "faire le ménage" après le déclenchement d'une exception sans pouvoir gérer complètement l'exception. 
Par exemple, la fonction GetFileList alloue un objet TStringList et le remplit avec les noms de fichiers correspondant au chemin de recherche spécifié :

function GetFileList(const Path: string): TStringList;
var
  I: Integer;
  SearchRec: TSearchRec;
begin
  Result := TStringList.Create;
  try
    I := FindFirst(Path, 0, SearchRec);
    while I = 0 do
      begin
          Result.Add(SearchRec.Name);
          I := FindNext(SearchRec);
      end;
  except
      Result.Free;
      raise;
  end;
end;

GetFileList crée un objet TStringList puis utilise les fonctions FindFirst et FindNext (définies dans SysUtils) pour l'initialiser. Si l'initialisation échoue (car le chemin d'initialisation est incorrect ou parce qu'il n'y a pas assez de mémoire pour remplir la liste de chaînes), c'est GetFileList qui doit libérer la nouvelle liste de chaînes car l'appelant ne connaît même pas son existence. C'est pour cela que l'initialisation de la liste de chaînes se fait dans une instruction try...except. Si une exception a lieu, le bloc exception de l'instruction libère la liste de chaînes puis redéclenche l'exception.
Exceptions imbriquées

Le code exécuté dans un gestionnaire d'exception peut lui aussi déclencher et gérer des exceptions. Tant que ces exceptions sont également gérées dans le gestionnaire d'exception, elles n'affectent pas l'exception initiale. Par contre, si une exception déclenchée dans un gestionnaire d'exception commence à se propager au-delà du gestionnaire, l'exception d'origine est perdue. Ce phénomène est illustré par la fonction Tan suivante.

type
   ETrigError = class(EMathError);
   function Tan(X: Extended): Extended;
   begin
      try
        Result := Sin(X) / Cos(X);
      except
        on EMathError do
        raise ETrigError.Create('Argument incorrect pour Tan');
      end;
   end;

Si une exception EMathError se produit lors de l'exécution de Tan, le gestionnaire d'exception déclenche une exception ETrigError. Comme Tan ne dispose pas de gestionnaire pour ETrigError, l'exception se propage au-delà du gestionnaire d'exception initial, ce qui provoque la destruction de l'objet exception EMathError. Ainsi, pour l'appelant, tout se passe comme si la fonction Tan avait déclenché une exception ETrigError.
Instructions try...finally

Dans certains cas, il est indispensable que certaines parties d'une opération s'effectuent, que l'opération soit ou non interrompue par une exception. Si, par exemple, une routine prend le contrôle d'une ressource, il est souvent important que cette ressource soit libérée quelle que soit la manière dont la routine s'achève. Vous pouvez, dans ce genre de situations, utiliser une instruction try...finally. 

L'exemple suivant illustre comment du code qui ouvre et traite un fichier peut garantir que le fichier est fermé, même s'il y a une erreur à l'exécution.

Reset(F);
try
   ... // traiter le fichier F
finally
   CloseFile(F);
end;

Une instruction try...finally a la syntaxe suivante : 

Try listeInstructions1 finally listeInstructions2 end 

où chaque listeInstructions est une suite d'instructions délimitées par des points-virgule. L'instruction try...finally exécute les instructions de listeInstructions1 (la clause try). Si listeInstructions1 se termine sans déclencher d'exception, listeInstructions2 (la clause finally) est exécutée. Si une exception est déclenchée lors de l'exécution de listeInstructions1, le contrôle est transféré à listeInstructions2 ; quand listeInstructions2 a fini de s'exécuter, l'exception est redéclenchée. Si un appel d'une procédure Exit, Break ou Continue force la sortie de listeInstructions1, listeInstructions2 est exécutée automatiquement. Ainsi, la clause finally est toujours exécutée quelle que soit la manière dont se termine l'exécution de la clause try

Si une exception est déclenchée sans être gérée par la clause finally, cette exception se propage hors de l'instruction try...finally et toute exception déjà déclenchée dans la clause try est perdue. La clause finally doit donc gérer toutes les exceptions déclenchées localement afin de ne pas perturber la propagation des autres exceptions.

Classes et routines standard des exceptions

Les unités SysUtils et System déclarent plusieurs routines standard de gestion d'exceptions, dont ExceptObject, ExceptAddr et ShowException. SysUtils, System et d'autres unités contiennent également de nombreuses classes d'exceptions qui dérivent toutes (sauf OutlineError) de Exception. 
La classe Exception contient les propriétés Message et HelpContext qui peuvent être utilisées pour transmettre une description de l'erreur et un identificateur de contexte pour une aide contextuelle. Elle définit également divers constructeurs qui permettent de spécifier la description et l'identificateur de contexte de différentes manières.

Aucun commentaire: