Jouons un peu avec les types énumérés et leurs valeurs

Un type énuméré est une construction du langage qui permet d'avoir des noms en clair correspondant à des valeurs dans une liste. Par exemple cette liste de fruits :

type
  TFruits = (Orange, Pomme, Banane, Peche, Pasteque, Pamplemousse);

En C on parle du type ENUM
En Pascal on se contente d'écrire la liste.

Ces listes comme tout autre type scalaire peuvent être utilisées pour créer des ensembles. En Pascal ça donne :

type
  TCorbeilleDeFruits = set of TFruits;

Je ne vais pas m'étendre sur la question des ensembles pour cette fois-ci, mais vais rester sur les listes énumérées car j'ai eu un cas pratique il y a peu et ça pourrait vous servir aussi.

A quoi ça sert ?

Les listes énumérées permettent de simplifier le codage d'un programme. En effet il est toujours plus simple d'avoir un identifiant en clair dans le source qu'une valeur numérique. En cas de maintenance, savoir que la variable Bidule a pour valeur 3 est quand même moins simple à comprendre que de dire qu'elle a la valeur TFruits.Peche

Les types énumérés permettent donc de définir des sortes de constantes. Le compilateur les transforme en entiers en partant de 0.

Il est très important quand vous utilisez ces types de ne pas modifier l'ordre des valeurs si vous vous en servez dans des fichiers de paramétrage ou des bases de données car la valeur de chacun changerait et deviendrait par conséquent inutilisable avec les données antérieures.

Dans un projet récent j'ai eu besoin de stocker des variables dans un JSON. Le hic c'est que JSON ne connait que les nombres, les chaines de caractères, les booléens, les tableaux et les objets. La notion d'ensemble ou de valeur énumérée n'y existe pas. Il faut donc ruser un peu et se débrouiller pour convertir les fruits en quelque chose d'exploitable, mais aussi de pouvoir revenir au type de base quand on lit le JSON côté Delphi.

Je vous propose donc un petit programme d'exemple qui va vous donner les billes pour passer de l'un à l'autre assez facilement. Ce programme est disponible sur mon compte GitHub.

Première étape : parcourir les valeurs d'un type énuméré, en récupérer l'indice et le texte.

Inutile de revenir sur l'utilisation quotidienne des types énumérés. La plupart des propriétés des composants fournis avec Delphi en sont. Vous les utilisez au quotidien dans votre programmation.

Ce que vous faites plus rarement, c'est de jouer avec le nom de leurs valeurs en dehors des programmes, hors c'est très utile pour transférer des informations à un autre programme, dans un autre langage, sur un autre ordinateur, ou tout simplement pour sauvegarder / restaurer des données. La encore Delphi en use à donf rien qu'avec les sources des fiches (les fichiers DFM et FMX).

La première des choses à faire est d'ajouter l'unité System.TypInfo à votre programme. Vous pouvez ensuite utiliser les fonctions GetEnumValue et GetEnumName.

Pour lister les valeurs possibles d'un type énuméré, il n'y a bizarrement pas d'énumérateur. Il faut passer par une boucle FOR en utilisant la valeur minimale et la valeur maximale du type.

var
  fruit: TFruits;
begin
  for fruit := low(TFruits) to high(TFruits) do
  begin
    ShowMessage(GetEnumName(typeinfo(TFruits), ord(fruit)));
  end;
end;

Cette boucle va permettre d'afficher les différents fruits présents dans TFruits, dans l'ordre saisi dans le programme et avec la même typo.

low(TFruits) correspond à la première valeur de TFruits. Ici à TFruits.Orange dont seul "Orange" est retourné par GetEnumName.
high(TFruits) correspond à la plus grande valeur possible. Ici c'est donc TFruits.Pamplemousse.
ord(fruit) prend l'indice du fruit dans son type énuméré. Pour Orange ce sera donc 0 et pour Pamplemousse ce sera 5.

Attention : avec GetEnumName les majuscules et minuscules seront sorties comme elles sont saisies dans vos fichiers sources. Si vous stockez le nom d'une valeur quelque part et désirez ensuite le comparer avec quelque chose d'autres, il sera sensible à la casse. Pensez-y !

Maintenant que l'on sait comment récupérer le libellé associé à une valeur, passons à l'étape inverse : récupérer la valeur associée à un libellé.

Seconde étape : partir d'une chaine de caractères pour trouver le type énuméré

La fonction GetEnumName fournit le libellé d'une valeur. La fonction GetEnumValue permet de revenir à la valeur à partir du libellé.

En fait quand je dis "à la valeur", c'est faux. On revient à l'indice de la valeur dans la liste énumérée. Ca fait une grosse différence car nous ne pouvons pas l'utiliser tel quel dans une variable fruit qui serait de type TFruits.

var
  fruit: TFruits;
begin
fruit := 2; // erreur
fruit := TFruits.Banane; // correct
end;

En utilisant GetEnumValue(typeinfo(TFruits),'Banane') on obtient la valeur entière 2 qui correspond à l'indice de Banane dans TFruits.

Problème : ce 2 est sans doute bien gentil, mais qu'est-ce qu'on en fait . Dans notre programme il faut TFruits.Banane et non 2...

Et c'est là qu'intervient notre côté McGyver.

Notez que si le texte ne correspond à rien GetEnumValue() renvoit l'indice -1.

Troisième étape : partir d'une valeur numérique pour déterminer l'élément dans le type énuméré

On sait obtenir l'indice d'un élément de la liste à partir de son nom et de sa valeur. On peut donc retrouver l'un en le comparant à l'autre.

  id := GetEnumValue(typeinfo(TFruits), Edit1.Text);
  ok := false;
  for fruit := low(TFruits) to high(TFruits) do
    if ord(fruit) = id then
    begin
      ok := true;
      break;
    end;

A partir du texte de la valeur désirée, on obtient son ID dans la liste. Puis en parcourant les valeurs possibles on s'arrête au bon indice et on obtient ainsi la valeur exploitable dans le programme.

En partant du texte 'Banane' qui donnera 2 comme indice, dans cet exemple quand ok est vrai alors fruit vaudra bien TFruits.Banane.

Suite à la lecture de ce paragraphe, Didier Cabalé m'a justement fait remarquer qu'il y avait une autre solution plus standard pour obtenir la valeur depuis son indice : le transtypage. Vous pouvez donc remplacer le code précédent par ces lignes:

  id := GetEnumValue(typeinfo(TFruits), Edit1.Text);
  fruit := TFruits(id);

Par contre le code avec parcours de liste permettait de s'assurer de trouver une valeur ou de savoir qu'elle n'existe pas. Avec le transtypage, si l'ID ne correspond à rien on n'obtient ni erreur ni exception mais une valeur incohérente (avec la version Delphi 10.2.1 Tokyo). Prenez vos précautions.

C'est bien beau, mais on en fait quoi ?

Grâce à ça vous avez deux possibilités pour gérer le chargement et la sauvegarde de données correspondant à un type énuméré : stocker l'indice de la valeur ou son libellé. Les deux passent dans du JSON, dans un fichier texte ou dans un champ de base de données (soit numérique, soit alphanumérique).

La seule contrainte à bien respecter est de ne jamais modifier le type énuméré une fois que le programme a été utilisé pour ne pas perturber l'indice des valeurs qui auraient déjà été stockées chez l'un de vos utilisateurs.
En pratique si vous ajoutez des valeurs possibles, il faut les ajouter toujours en fin de liste.

Pour être complet je dois ajouter ceci : on peut utiliser les types énumérés pour y stocker des listes de constantes ce qui permet de fixer les éléments de la liste dans l'ordre que l'on veut et avec la valeur que l'on veut. En revanche en faisant ça getEnumValue et getEnumName ne fonctionnent plus du coup plus besoin de l'unité System.TypInfo. En revanche on a toujours la possibilité d'utiliser low() et high() pour une boucle et ord() pour obtenir la valeur entière et non plus l'indice qui nous intéresse.

type
  TFruitsConst = (OrangeConst = 3, PommeConst = 18, BananeConst = 15,
    PecheConst = 0, PastequeConst = 12, PamplemousseConst = 7);

var
  fruit, trouve: TFruitsConst;
  id: Integer;
begin
  fruit := TFruitsConst.BananeConst;
  id := ord(fruit); // 15 et non 2 comme on s'y attendrait
  showmessage(id.ToString);
  for fruit := low(TFruitsConst) to high(TFruitsConst) do
  begin
    if (ord(fruit) = id) then
    begin
      trouve := fruit;
      break;
    end;
  end;
  showmessage(ord(trouve).ToString); // affiche 15
end;

Grâce à cette technique vous pouvez stocker des valeurs communes entre vous et d'autres programmes tout en les gérant de façon plus lisible dans vos programmes.

Redéfinir des constantes pour s'en sortir est très utile lorsqu'on travaille avec des API fournies par des tiers. Delphi en est bourré, vous pouvez en abuser vous aussi.


A lire aussi

Jouons un peu avec les types énumérés et leurs valeurs (05/12/2017)
Simplifier le passage de listes en paramètres et leur création (24/07/2017)