La gestion des exceptions et le contrôle d'erreurs sous Delphi

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 quelque 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.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Le présent tutoriel n'a pas la prétention de tout vous apprendre sur la gestion des exceptions (comme tous les tutoriels que je fais mais celui-là plus que les autres). Mon but est de vous aider à ne plus rechigner à les utiliser, en vous donnant tout ce que vous avez besoin de savoir pour vous sentir à l'aise avec la bête. 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/modifier ce tutoriel).

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 quelque 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 saisisse tel type de donnée dans telle variable (un champ de saisie où il faut entrer des nombres). Par utilisateur, je considère aussi bien une personne que 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). D'autre diront que c'est mieux de faire ainsi parce que c'est plus classe (là aussi à vous de voir)

Et justement, ce cours a pour but de vous permettre de vous faire une opinion :)

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 type classe de conversion (EConverError).

La protection d'un bloc de code se fait par l'utilisation d'un couple de mot clé. Ces couples sont : 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 qui suivent le second mot clé du couple. Un peu comme un if/else, sauf qu'au lieu du test, on surveille les erreurs éventuelles entre le if et le else. La différence entre le couple try..finally et try..except se situe dans la condition d'exécution du second bloc d'instruction et dans la suite de l'exécution du programme.

Les deux couples de mots clés sont utilisés de manière tout à fait différente car ils ont une condition d'exécution différente et un comportement différent.

  • Le couple try..finally est utilisé pour s'assurer qu'une opération sera toujours réalisée. En effet, peu importe qu'il est eu une erreur, les instructions entre le finally et son end; seront exécutées.
    D'ailleurs, l'erreur n'est pas du tout traité par un couple try..finally. L'unique but de ce couple est de garantir que le code entre finally et son end sera exécuté. Quant à l'erreur, si elle ne trouve pas un couple try..except, elle est remontée à la fonction appelante jusqu'à ce qu'elle soit traitée. Dans le pire des cas, l'erreur est affiché exactement comme lorsque le programme n'est pas protégé.
  • Avec try..except, les instructions du second bloc d'instruction ne sont exécutées que s'il y a eut une erreur entre le try et le except. Le but de ce couple sert effectivement à gérer les erreurs. try..except propose un mécanisme pour traiter, l'exception en fonction de la classe d'exception. Autrement dit, de traiter l'erreur en fonction de sa nature.

Pour vous faire une idée plus concrète de la chose, voici la syntaxe pour try..finally et try..except.

Try..Finally
Sélectionnez
    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 s'il n'y a pas eu d'erreur plus haut.
    instructions3
Try..Except
Sélectionnez
    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, même en cas d'erreur plus haut car déjà traitée.
    instructions3

Vous l'aurez remarqué, le bloc dit protégé est celui qui le paraît le moins. En fait on devrait dire bloc à protéger, c'est-à-dire qu'il s'agit d'une suite d'instruction pouvant causer des exceptions. 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. Nous verrons cela plus en détail pour la partie try..except.

II. Try..Finally

La syntaxe de try..finally est :

 
Sélectionnez
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.

Le programme poursuit ensuite normalement son exécution s'il n'y a pas eu d'erreur sinon l'erreur est remontée au prochain except. S'il n'y a aucun couple try..except pour gérer l'erreur, le programme se conduit comme d'habitude en cas d'erreur et vous gratifie d'un message sibyllin. (Rassurez-vous, vous aurez toutes les pièces en main avec la partie try..except.)

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
Sélectionnez
procedure TForm1.Button1Click(Sender: TObject);
var
    Ob : TObjetExemple;
begin
    Ob := TObjetExemple.Create; // Création de l'objet, en dehors du try..finally
    try
        // Manipulation de l'objet
        {instructions}
    finally
        Ob.Free; // Libération de l'objet
    end;
end;
Exemple : assignation d'un fichier
Sélectionnez
function OuvrirF(Nom : TFileName) : boolean;
var
    F : Textfile;
    S : string;
    i, j, valeur :integer;
begin
    AssignFile(F,Nom); // Ouverture du fichier, en dehors du try..finally
    try
        // Manipulation du fichier
        Reset(F);
        readln(F,S);
        {instructions}
        Result := True;
    finally
          CloseFile(F); // Libération du fichier
    end;
end;

Notez la position de demande d'allocation de ressource par rapport au try..finally.

Attention :
Tant que vous n'avez pas appelé CloseFile(F), vous ne pouvez plus 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ées, périphérique…). D'où l'importance de s'assurer de leur libération.

III. Try..Except

III-A. Grammaire

Commençons par un simple et classique :

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 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 variable, 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 :

 
Sélectionnez
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 aux instruction3. Contrairement au try..finally, la suite de l'exécution se passe normalement, l'erreur ayant été traitée.

Exemple : Gestion des erreurs liées à une conversion de Type.
Sélectionnez
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 à :

Image non disponible
Figure 1Exemple Try..Except

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 est 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, c'est le type d'erreur soulevée par une fonction particulière).

Pour les erreurs imprévues, on peut utiliser un simple else, qui exécutera une suite d'instruction par défaut. Par exemple afficher un message d'erreur pour l'utilisateur avec des causes possibles de la raison.

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

 
Sélectionnez
try
    {instructions}
except
    {instructions éventuelles communes à 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;

Un exemple complet :
Rappel sur l'utilisation du finally. Son utilisation permet de s'assurer qu'une suite d'instruction va être réalisée même en cas d'erreur dans le code situé avant. Mais l'erreur doit être traitée par un couple try..except.

Pour vous aidez à bien comprendre, je vous propose l'exemple concret ci-dessous.

C'est un extrait du cours sur TFileStream, mais pour éviter d'aller voir, ce qui est un tort :) , voici une petite explication. En gros la fonction ouvre un fichier et place le contenu dans une variable de type record (mrec). Pour que les choses se passe bien, le fichier doit exister, on doit pouvoir l'ouvrir, etc. De plus, lorsque le fichier est ouvert par notre application (les autres aussi), il est indisponible pour d'autres applications. Il nous faut donc nous assurer qu'on le fermera dès qu'il ne sera plus nécessaire. Grâce au couple try..except et try..finally, nous faisons tout ça.

Exemple du traitement de l'erreur et de l'utilisation du finally
Sélectionnez
function OuvrirFichier(Nom : TFileName;var mrec : TMonRecord) : boolean;
var
   F : TFileStream;
   i : integer;
begin
    // Ouvre un fichier et affecte le contenu à un record
    F := nil;
    try // Ce try est lié à l'except. Par son intermédiaire, on va traiter les erreurs
      // Ouverture d'un flux sur le fichier en lecture. 
      // Le fichier reste accessible en lecture seule pour d'autres appli.
      F := TFileStream.Create(Nom, fmOpenRead or fmShareDenyWrite);
      try // ce try ci est lié au finally. Ici on veut s'assurer que le fichier soit libéré quoiqu'il advienne.
        // Vous remarquez que la demande de création est extérieur au try..finally, n'est ce pas ?
        // En effet, il n'y a rien à libérer si cela n'a pas été créé. Le try..except gérera ce cas là.
        // En plus, try..except géra une erreur éventuel survenue entre try..finally.
        
        // Je vous laisse le code de lecture notamment pour que vous réalisez que ce code pourrait planter.
        // Si vous regardez bien, vous verrez plusieurs raisons possibles :
        // Le nombre de champ peut ne pas être celui attendu,
        // Des erreurs de conversions peuvent survenir, etc.
        F.Position := 0; // Début du flux
        if (pos('.zio',ExtractFileName(Nom))>0) then // Vérification du type du Fichier
        begin
            // Lecture du fichier
            while (F.position < F.Size) do // Tant que la fin du fichier n'est pas atteinte faire :
            begin
            with mrec do // voir TMonRecord pour savoir quelle type de donnée nous avons.
            begin
               // Lit la valeur et déplace la position en cours
               // La première valeur lue est un entier (le fichier a été enregistré ainsi)
               F.ReadBuffer(entier, SizeOf(integer));
               // Ensuite nous avons un réel
               F.ReadBuffer(reel, SizeOf(Double));
               // De nouveau un entier mais sa valeur n'est pas directement exploitable dans mrec.
               // On le convertit, ici il s'agit d'un type énuméré.
               F.ReadBuffer(i, SizeOf(integer));
               enum := TEnumTest(i);
               // On lit ensuite une Chaine de caractère dont la taille est limitée (string[taille]).
               F.ReadBuffer(chainelimit, SizeOf(Chainelimit));
               // On lit un entier correspondant à la taille de la chaine qui suit
               F.ReadBuffer(i, SizeOf(i));
               // Allocation d'assez d'espace dans la chaine pour la lecture
               SetLength(chaine, i);
               // Lecture de la chaine, on transmet un pointeur sur la chaine soit Chaine[1].
               F.ReadBuffer(chaine[1], i);
            end;
            end;
            Result := true;
        end
        else
        begin
            MessageDlg('Erreur ce n''est pas un fichier test.', mtError, [mbOk], 0);
            Result := false;
        end;
      finally
        // Ici, on libère le fichier. Comme la libération est dans la partie finally,
        // on est sûr que le fichier sera bien libérer.
        F.Free;
      end; // fin try..finally
    except
      // Là, on traite les erreurs.
      Result := False;
      on EInOutError do
      begin        
        MessageDlg('Erreur d''E-S fichier.', mtError, [mbOk], 0);
      end;
      on EReadError do
      begin
         MessageDlg('Erreur de lecture sur le fichier.', mtError, [mbOk], 0);
      end;
      else
      begin
         // Ici, on traite les erreurs auquel on n'a pas pensé ou dont on ne veut pas s'encombrer
         // par exemple, les erreurs de conversions.
         MessageDlg('Erreur sur le fichier.', mtError, [mbOk], 0);
      end;
    end; // fin try..except
    // Les erreurs ont été traité, le programme continuera donc normalement. D'autant plus, que nous renvoyons
    // le résultat de l'opération (result)
end;

Précision :
La protection d'un bloc de code par try..except 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.

III-B. 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 bornes 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.

IV. 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 tests 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. Ce qui n'est 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 :

 
Sélectionnez
type
  MonException = class(Exception)
    HelpContext : THelpContext; // Contexte dans l'aide
    Message : string; // Message d'erreur
  public
    procedure FonctionGerantErreur(); // Fonction à appeler en cas d'erreur
  end;
procedure 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.

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.

 
Sélectionnez
try
    {instructions}
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
Sélectionnez
type
   EValeurIncorrect = class(Exception);

et dans le code

 
Sélectionnez
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.

V. 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 comme vous avez pu le voir plus haut).

Exemple de déclenchement d'exception personnalisée.
Sélectionnez
if Valeur <> ValeurCorrect then
    raise EValeurIncorrect.Create('Valeur ne fait pas partie des valeurs autorisées');
Exemple complet de l'utilisation de raise
Sélectionnez
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;  // Attention n'oubliez pas ce point virgule (on est pas dans le cas if/else !)
    else
    begin
       ShowMessage('Erreur Inconnue');
       tmpstr := 'Invalid';
    end;
   end;
   ShowMessage('Résultat : '+tmpStr);
end;
end;

VI. 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 tutoriel a été pour vous une mine d'information et que désormais vous aborderez la gestion des exceptions avec sérénité.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Tony Baheux. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.