Exceptions (Delphi)
Remonter à Classes et objets - Index
Cette rubrique couvre les sujets suivants :
- Présentation conceptuelle des exceptions et de la gestion des exceptions
- Déclaration des types exception
- Déclenchement et gestion des exceptions
Sommaire
A propos des exceptions
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'exceptions, 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. De nombreuses 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 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 ou qui sont difficiles à évaluer, 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); // raises an EInOutError exception if file is not found except on Exception do ... end;
Mais vous pourriez aussi éviter la lourdeur du système de gestion des exceptions en utilisant :
if FileExists(FileName) then // returns False if file is not found; raises no 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 SysUtils.EAssertionFailed. Les assertions ne devraient ê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 SysUtils.Exception définie dans SysUtils.
Vous pouvez regrouper les exceptions dans des 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é la nature de ces déclarations, vous pouvez définir un seul gestionnaire d'exceptions SysUtils.EMathError qui gère également SysUtils.EInvalidOp, SysUtils.EZeroDivide, SysUtils.Overflow et SysUtils.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 object at address
où object et at address 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('Missing parameter') 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 SysUtils.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 is declared in SysUtils if (Result < Min) or (Result > Max) then raise ERangeError.CreateFmt('%d is not within the valid range of %d..%d', [Result, Min, Max]); end;
Remarquez la méthode CreateFmt appelée dans l'instruction raise. SysUtils.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.
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 SysUtils.EZeroDivide est déclenchée.
L'instruction try...except a la syntaxe suivante :
try statements except exceptionBlock end
où statements est une suite d'instructions (délimitée par des points-virgules) et exceptionBlock est soit :
- une autre suite d'instructions, soit
- une suite de gestionnaires d'exceptions, éventuellement suivie par
else statements
Un gestionnaire d'exceptions a la forme :
on identifier: type do statement
où identifier: est facultatif (si ce dernier est précisé, ce doit être un identificateur valide), type est le type utilisé pour représenter les exceptions et statement est une instruction quelconque.
Une instruction try...except exécute les instructions dans la liste initiale d'instructions. Si aucune exception n'est déclenchée, le bloc exception (exceptionBlock) est ignoré 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 d'instructions initiale, que ce soit par une instruction raise dans la liste d'instructions ou par une procédure ou une fonction appelée dans la liste d'instructions, il y a une tentative de "gestion" de l'exception :
- Si un des gestionnaires du bloc exception correspond à l'exception, le contrôle passe au premier d'entre eux. Un gestionnaire d'exception "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 si 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'exceptions 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'exceptions, l'objet exception est quand même détruit automatiquement.
Dans l'exemple suivant, le premier gestionnaire d'exceptions 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. SysUtils.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'exceptions 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 qui suit 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 (SysUtils.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 :
- Utilisez le mot réservé raise brut pour redéclencher l'objet exception en cours. Cela permet à un gestionnaire d'exception de répondre à une erreur d'une manière partielle, puis de redéclencher l'exception. Cela s'avère 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 :
try raise Exception.Create('Error Message'); except on E: Exception do begin E.Message := E.Message +sLineBreak+ 'FATAL'; raise; end; end;
- D'autre part, ne redéclenchez PAS un objet exception en cours en utilisant raise E. Utilisez raise à la place. Sinon, une violation d'accès peut survenir. Ceci est incorrect :
try raise Exception.Create('Error Message'); except on E: Exception do begin E.Message := E.Message +sLineBreak+ 'FATAL'; raise E; end; end;
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'exceptions peut lui aussi déclencher et gérer des exceptions. Tant que ces exceptions sont également gérées dans le gestionnaire d'exceptions, elles n'affectent pas l'exception initiale. Par contre, si une exception déclenchée dans un gestionnaire d'exceptions 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('Invalid argument to Tan'); end; end;
Si une exception SysUtils.EMathError se produit lors de l'exécution de Tan, le gestionnaire d'exceptions déclenche une exception ETrigError. Comme Tan ne dispose pas de gestionnaire pour ETrigError, l'exception se propage au-delà du gestionnaire d'exceptions initial, ce qui provoque la destruction de l'objet exception SysUtils.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. Dans ces situations, vous pouvez 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 ... // process file F finally CloseFile(F); end;
L'instruction try...finally a la syntaxe suivante :
try statementList1 finally statementList2 end
où chaque statementList est une suite d'instructions délimitées par des points-virgules. L'instruction try...finally exécute les instructions de statementList1 (la clause try). Si statementList1 se termine sans déclencher d'exception, statementList2 (la clause finally) est exécutée. Si une exception est déclenchée lors de l'exécution de statementList1, le contrôle est transféré à statementList2 ; quand statementList2 a fini de s'exécuter, l'exception est redéclenchée. Si un appel des procédures Exit, Break ou Continue force la sortie de statementList1, statementList2 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, notamment 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 SysUtils.Exception.
La classe SysUtils.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.