Les énumérations sous Delphi .NET

Les énumérations du framework .NET me semblaient être un type de donnée différent mais il n'en est rien, elles se manipulent de la même manière qu'une énumération déclarée sous Delphi .NET.

Vous trouverez dans ce tutoriel les manipulations de base sur les énumérations sous Delphi .NET ainsi qu'une approche de la technique d'injection de code CIL au travers de la classe EnumBuilder.

Je tiens à remercier Olivier Lance (Bestiol) pour ses corrections orthographiques et relectures attentives.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

1. Public concerné

Image non disponible

Testé sous le framework .NET 1.1 et Delphi 2005.
Version 1.0

1-1. Les sources

Les fichiers sources des différents exemples :
FTP.
HTTP.

2. Les bases

Projet : ..\Enum\BasesEnumeration

Sous Delphi une énumération est un type simple de type scalaire :

 
Sélectionnez

Les valeurs possibles d'un type scalaire forment un ensemble ordonné; à chaque valeur est associé un rang 
qui est une valeur entière.
La première valeur à le rang zéro et l'ordre est celui défini lors de la déclaration.
Les identificateurs d'une énumération sont considérés comme des constantes du type énuméré.

Dans l'exemple suivant l'énumération Couleurs débute avec la valeur zéro, c'est-à-dire la constante nommée Bleu :

 
Sélectionnez

Type
  Couleurs = (Bleu,Blanc,Noir,Rouge,Vert);

Voici les instructions autorisées sur une énumération : Ord, Succ, Pred, Low, High

Un exemple de leurs utilisation :

 
Sélectionnez

Type
   // Enumération débutant à l'indice zéro
  Couleurs = (Bleu,Blanc,Noir,Rouge,Vert);

Function GetCouleur(EtatCourant : Couleurs):String;
// Affiche une string correspondant au contenu de la variable énumération EtatCourant
begin
 case EtatCourant of
   Bleu  : Result:='Bleu';
   Blanc : Result:='Blanc';
   Noir  : Result:='Noir';
   Rouge : Result:='Rouge';
   Vert  : Result:='Vert';
 end;
end;

Var uneCouleur : Couleurs;
    I: Byte;

begin
 Write('Affectation : ');
 uneCouleur:=Bleu;
 Writeln(GetCouleur(uneCouleur));

 Write('Valeur suivante : ');
 uneCouleur:=Succ(uneCouleur);
 Writeln(GetCouleur(uneCouleur));

 Write('Valeur précédente :');
 uneCouleur:=Pred(uneCouleur);
 Writeln(GetCouleur(uneCouleur));

 Writeln;
 Write('Affectation : ');
 uneCouleur:=Bleu;
 Writeln(GetCouleur(uneCouleur));

 Write('Incrémente : ');
 Inc(UneCouleur);
 Writeln(GetCouleur(uneCouleur));

 Write('Décrémente : ');
 Dec(UneCouleur);
 Writeln(GetCouleur(uneCouleur));

 Write('Obtient la valeur minimum : ');
 Writeln(GetCouleur(Low(Couleurs)));

 Write('Obtient la valeur maximum : ');
 Writeln(GetCouleur(High(Couleurs)));

 Writeln;
 Write('Accès par indice (3) : ');
 Writeln(GetCouleur(Couleurs(3)));  // Débute à zéro

 Write('Liste ordonnée des valeurs : ');
 For I:=byte(Low(Couleurs)) to Byte(High(Couleurs)) do
  begin
   Write(GetCouleur(Couleurs(I)));
   if I<>Byte(High(Couleurs))
    then Write(', ');
  end;
 Readln;
end.

Projet : ..\Enum\BasesEnumeration2

La fonction GetCouleur n'est plus nécessaire si on utilise les fonctionnalités objet du framework :

 
Sélectionnez

 Write('Affectation : ');
 uneCouleur:=Bleu;
   // Transtypage obligatoire, Boxing
 Writeln(TObject(uneCouleur));

On remplace tous les appels de la fonction GetCouleur(uneCouleur) par TObject(uneCouleur).

Vous pouvez aussi consulter ce tutoriel au sujet des énumérations (Delphi Win32).

3. Une nouvelle approche

Dans le premier exemple le type Couleurs est une énumération mappée sur System.Enum de type Byte. On peut déclarer une énumération débutant avec un indice supérieur à zéro :

 
Sélectionnez

type
  Conteneur= (Carton= 52, Valise= 137); // La valeur détermine le type sous jacent.

Ici l'énumération Conteneur débute à l'indice 52 et se termine à 137, son type de base est donc Byte. Si on modifie la dernière constante en lui affectant 257 son type de base sous-jacent est modifié en Word. Il ne semble pas possible comme en C# de préciser le type de base de l'énumération en dehors de cette construction.

 
Sélectionnez

 // Définit une énumération de type LONG
 enum Range :long {Max = 2147483648L, Min = 255L};

L'appel de la méthode Enum.GetUnderlyingType permet de retrouver le type sous-jacent :

 
Sélectionnez

var  UneBoite : Conteneur;
begin
  // Affiche le type sous-jacent d'une variable énumération
 Writeln('Type sous-jacent de l''énumération UneBoite : ');
    //Appel sur une variable énumération
 Writeln(Enum.GetUnderlyingType(TObject(UneBoite).GetType));
    // ou directement
 Writeln(Enum.GetUnderlyingType(TypeOf(UneBoite)));
    //Appel sur un type énumération
 Writeln(Enum.GetUnderlyingType(Typeof(Conteneur)));

La recherche d'une constante d'une énumération se fait via la méthode Enum.isDefined :

 
Sélectionnez

  //Teste si la valeur spécifiée existe dans une énumération.
 If System.Enum.isDefined(TypeOf(Conteneur),TObject(1))
  then Writeln('la valeur 1 existe dans l''énumération Conteneur')
  else Writeln('la valeur 1 n''existe pas dans l''énumération Conteneur');

  // La recherche est 'case sensitive'
 If System.Enum.isDefined(TypeOf(Conteneur),'valise')
  then Writeln('la valeur valise existe dans l''énumération Conteneur')
  else Writeln('la valeur valise n''existe pas dans l''énumération Conteneur');

Attention cette méthode de recherche est case-sensitive.

L'usage d'un itérateur est possible sur une énumération. Dans l'exemple suivant on manipule des chaînes des caractères représentant les constantes nommées :

 
Sélectionnez

var Chaine : String;
begin
 // Affiche les valeurs possibles de l'énumération
 // Récupère un tableau de chaînes de caractères
 for Chaine in Enum.GetNames(typeof(Couleurs)) do
  begin
   WriteLn(Chaine);
  end;

Pour récupérer et manipuler chaque entrée de l'énumération on utilise la méthode Enum.GetValues :

 
Sélectionnez

var UneBoite : Conteneur;
begin
   // Affiche les valeurs possibles de l'énumération
   // Récupére un tableau d'objets
  for UneBoite in Enum.GetValues(typeof(Couleurs)) do
  begin
   WriteLn(TObject(UneBoite));
  end;

4. Manipulation des énumérations du framework

Projet : ..\Enum\BasesEnumeration3


Les énumérations du framework peuvent être manipulées comme celles déclarées sous Delphi :

 
Sélectionnez

Type
  //Alias, facilite l'écriture
 TEnvFolder = Environment.SpecialFolder;

Var  Chaine  : String;
     Folder : Environment.SpecialFolder;

begin

 //Environment.SpecialFolder
 Folder:=TEnvFolder.ApplicationData;
 Writeln(TObject(Folder));

 // Accès direct
 Writeln(TObject(TEnvFolder.ApplicationData));

 Writeln(Ord(TEnvFolder.ApplicationData));

 Writeln(TObject(Succ(TEnvFolder.ApplicationData)));
 Folder:=Succ(Succ(TEnvFolder.ApplicationData));
 Writeln(TObject(Folder));

 Folder:=Pred(Folder);
 Writeln(TObject(Folder));

 Writeln(TObject(Low(Environment.SpecialFolder)));
 Writeln(TObject(High(Environment.SpecialFolder)));

 Writeln(TObject(Environment.SpecialFolder(5))); // 5=Personnal
 Writeln;

 // Affiche les valeurs possibles de l'énumération
 for Chaine in Enum.GetNames(typeof(Environment.SpecialFolder)) do
  begin
   Write(Chaine+' , ');
  end;

  // Test de valeur
 if Folder=TEnvFolder.ApplicationData
  then WriteLn;

 readln;

Il est possible de tester si une valeur existe dans une énumération avec le code suivant :

 
Sélectionnez

 if ((flagAttr and FileAttributes.Archive) = FileAttributes.Archive)
  then Console.WriteLine(FileAttributes.Archive);

Pour tester toutes les valeurs d'une énumération vous pouvez aussi utiliser un itérateur :

 
Sélectionnez

procedure GetFileAttributes(NomFichier : String) ;
//  Test : GetFileAttributes('C:\IO.SYS');
var flagAttr : FileAttributes;
    unAttr : System.IO.FileAttributes;

begin
 With System.IO.File do
  begin
   flagAttr := GetAttributes(NomFichier);

   for UnAttr in Enum.GetValues(TypeOf(System.IO.FileAttributes)) do
    if ( (flagAttr and unAttr) = UnAttr)
     then Console.WriteLine(UnAttr);
  end;
end;

5. Du nouveau : les champs de bits

Projet : ..\Enum\GestionEnum

Il est possible de déclarer une énumération optimisée par l'utilisation de l'attribut [FlagsAttribute], dans ce cas elle est traitée comme un champ de bits, c'est-à-dire un ensemble d'indicateurs :

 
Sélectionnez

  [FlagsAttribute]  
  DroitFichier = (Modification= 1,
                  Lecture = 2,
                  Ecriture= 4,
                  Effacement= 8,
                  All=15);

Les champs de bits sont généralement utilisés pour des listes d'éléments qui peuvent être combinées alors que les constantes énumérées sont généralement utilisées pour des listes d'éléments qui s'excluent mutuellement.

L'affectation se fait soit avec l'opérateur OR (son usage n'est pas possible avec des constantes énumérées) :

 
Sélectionnez

Var Droits : DroitFichier;
begin
  // Champs de Bit
 Droits := Lecture;
 Writeln(TObject(Droits));

 Droits := Droits or Ecriture;
 Writeln(TObject(Droits));

  //Affichage au format String
 Writeln(TObject(Droits));
 Writeln(Format('Resultat=%s',TObject(Droits)));

  //Affichage au format numérique
 Writeln(Convert.ToInt32(Droits));
 Writeln(Format('Resultat=%d',TObject(Droits)));
 Writeln;

soit avec une des méthodes surchargées de Enum.Parse :

 
Sélectionnez

  //Affectation via une chaîne de caractères
 NouveauDroit:='Ecriture,Effacement';
 Droits:=DroitFichier(System.Enum.Parse(TypeOf(DroitFichier),NouveauDroit));
  // OU Resultat:=(System.Enum.Parse(TypeOf(DroitFichier),'Ecriture,Effacement')) as DroitFichier;
 Writeln(TObject(Droits));
 
  //Affectation via une valeur numérique
 DroitModifie:= DroitFichier(System.Enum.ToObject(TypeOf(DroitFichier),4));
 WriteLn(TObject(DroitModifie));
 
  //Ajout d'une valeur à une énumération existante
 NouveauDroit:=','+'Lecture';
 Droits:=DroitFichier(System.Enum.Parse(TypeOf(DroitFichier),Tobject(Droits).ToString+NouveauDroit));
 Writeln(TObject(Droits));
 

La méthode Parse permet aussi d'ajouter une valeur à une énumération existante :

 
Sélectionnez

 NouveauDroit:=','+'Lecture';
 Droits:=DroitFichier(System.Enum.Parse(TypeOf(DroitFichier),TObject(Droits).ToString+NouveauDroit));
 Writeln(TObject(Droits));

  //Ajoute une valeur String à une énumération existante
 NouveauDroit:=','+'Lecture';
 Droits:=DroitFichier(System.Enum.Parse(TypeOf(DroitFichier),TObject(Droits).ToString+NouveauDroit));
 Writeln(TObject(Droits));

  //'Retire' une valeur Numérique de l'énumération
 I:=Convert.ToInt32(Droits) And not Convert.ToInt32(Ecriture);
 Writeln(Format('Resultat int=%d',TObject(I))); 

soit avec les fonctions de l'unité TypInfo.pas qui appellent en interne la méthode Enum.Parse :

 
Sélectionnez

  // Utilisation des fonctions de l'unité TypInfo.pas
 Chaine:=GetEnumName(TypeInfo(DroitFichier),4); // 4=Ecriture
 Writeln('Recherche par valeur : ',Chaine);
 
 I:=GetEnumValue(TypeInfo(DroitFichier),'Ecriture');
 Writeln('Recherche par nom de constante : ',I.ToString);
 
 Droits:=DroitFichier(GetEnumObject(TypeInfo(DroitFichier),'Ecriture'));
 Writeln('Utilisation de GetEnumObject ',TObject(Droits));

Si on affecte une valeur représentant le résultat de l'addition de tous les champs, l'affichage n'est plus détaillé. Dans ce cas la méthode ToString appel en interne la méthode Format qui affichera la constante appropriée :

 
Sélectionnez

 Droits := Modification or Lecture or Ecriture or Effacement; // l'ensemble des valeurs est égal à All
 Writeln(TObject(Droits));
 Writeln(Format('Resultat =%s',TObject(Droits)));

dans ce cas l'affichage sera :

 
Sélectionnez

 Resultat = All

Pour retrouver les différentes valeurs associées à une énumération vous pouvez utiliser le code suivant :

 
Sélectionnez

for I:=0 to 512 do
 begin
  if TObject(Environment.SpecialFolder(I)).ToString<>I.ToString then
   Console.Writeline('{0} Value= {1}',[TObject(Environment.SpecialFolder(I)),I]);
 end;

L'appel de TObject(Environment.SpecialFolder(3)) affichera 3 étant donné que cette valeur n'est pas associée à une constante nommée dans l'énumération Environment.SpecialFolder.
Voir :
Le code source de System.Enum, attention il s'agit d'une préversion du framework .NET datant de 2000.
La classe Enum.

6. Construction dynamique d'une énumération avec EnumBuilder

Projet : ..\Enum\ExempleEnumBuilder

Le Framework .NET permet la génération de code à la volée appelé aussi injection de code CIL, il propose pour ce faire un ensemble de classes débutant par 'Builder'. Cette technique n'est pas détaillée dans ce tutoriel mais vous pouvez consulter ce lien sur le sujet ou mieux encore parcourir ce chapitre intitulé "La génération de code" issue du livre .NET de Dick Lantim.

Extrait de ce chapitre :

 
Sélectionnez

Pour maîtriser la génération de code à la volée, il convient de bien connaître la structure d'un assemblage.
Pour rappel, un assemblage est constitué d'un ou de plusieurs modules (c'est-à-dire un assemblage de 
NetModules). Chaque module dispose d'une ou de plusieurs classes. Chaque classe dispose de données membres 
et de méthodes.

Cet exemple est simplifié. On peut par exemple spécifier une version, une langue et un couple de clés publique/privée pour protéger le code IL généré.

Il est donc possible, au travers de la classe EnumBuilder, de construire dynamiquement une énumération à l'intérieur d'un assembly lui même créé dynamiquement.

La première étape consiste à créer un assemblage dans le domaine de l'application, domaine que l'on récupère par l'appel à la méthode Thread.GetDomain ou AppDomain.CurrentDomain :

 
Sélectionnez

const cstAssemblage='AssemblyDynamique'; // Nom de l'assembly
      AssemblyAcces = AssemblyBuilderAccess.RunAndSave; // Type d'accès de l'assembly

var NomAssembly : AssemblyName;
	MonDomain   : AppDomain
begin
  MonDomain:=Thread.GetDomain;
  // Affecte un nom pour l'assembly.
  NomAssembly:=AssemblyName.Create;
  NomAssembly.Name:=cstAssemblage;

La création du nouvel assemblage s'effectue par l'appel de la méthode DefineDynamicAssembly à partir de l'instance de domaine précédemment récupérée, le mode d'accés autorisera ou non certaines opérations :

 
Sélectionnez

Var  ConstructeurAssembly : AssemblyBuilder; // Définit et représente un assembly dynamique
begin
  ...
 ConstructeurAssembly:=MonDomain.DefineDynamicAssembly(NomAssembly,access);

Ensuite il nous faut créer un module par l'appel de la méthode DefineDynamicModule à partir de l'instance de l'assembly précédemment créé :

 
Sélectionnez

Var  ConstructeurModule : ModuleBuilder; // Définit et représente un module. Module Dynamique
begin
  ...
   // Créer un module dynamique persistant
  case Access of
    // si l'assembly est persistant nécessite un nom de module et un nom de fichier.
   AssemblyBuilderAccess.Save,
   AssemblyBuilderAccess.RunAndSave : 
     ConstructeurModule:= ConstructeurAssembly.DefineDynamicModule('ModuleDynamique','ModuleDynamique.mod');

    // si l'assembly n'est pas persistant nécessite uniquement un nom de module.
   AssemblyBuilderAccess.Run : 
     ConstructeurModule:= ConstructeurAssembly.DefineDynamicModule('ModuleDynamique');
  end;

Une fois l'assembly et le module créés, il nous reste à définir la classe souhaitée, ici une énumération, en appelant DefineEnum à partir de l'instance du module précédemment créé :

 
Sélectionnez

Var  ContructeurEnum : EnumBuilder; // Décrit et représente un type d'énumération
begin
  ...
  ContructeurEnum:= ConstructeurModule.DefineEnum('Enumeration.EnumerationDynamique',TypeAttributes.Public, typeof(Integer));

DefineEnum définit un type d'énumération qui est un type valeur avec un champ non statique unique appelé value__ du type spécifié.
Puis on passe à la définition des membres de la classe, ici il s'agit des constantes nommées, en appelant la méthode DefineLiteral qui définit un champ statique :

 
Sélectionnez

var myFieldBuilder1,
    myFieldBuilder2 : FieldBuilder;
    valeur1,
    valeur2 : Integer;
Begin
 ...
  valeur1:=1;
  valeur2:=2;
   // Le double transtypage est nécessaire
   //Définit le champ statique nommé d'un type d'énumération à l'aide de la valeur de constante spécifiée.
  myFieldBuilder1:= ContructeurEnum.DefineLiteral('Premier', TObject(Integer(1)));
  myFieldBuilder2:= ContructeurEnum.DefineLiteral('Deuxieme', TObject(Integer(2)));

   // Il est possible d'utiliser des variables :
    // myFieldBuilder1:= ContructeurEnum.DefineLiteral('Premier', TObject(valeur1));
    // myFieldBuilder2:= ContructeurEnum.DefineLiteral('Deuxieme', TObject(valeur2));

Le champ défini sera doté des attributs de champ Public, Static et Literal.

Le simple transtypage suivant :

 
Sélectionnez

 myFieldBuilder1:= ContructeurEnum.DefineLiteral('Premier', TObject(1));
 myFieldBuilder2:= ContructeurEnum.DefineLiteral('Deuxieme', TObject(2));

provoque l'exception ArgumentException :

 
Sélectionnez

"La constante ne correspond pas au type défini."

Et enfin on termine l'opération par la création de la classe et en récupérant son type.

 
Sélectionnez

var TypeDynEnum : System.Type;
begin
  ...
  TypeDynEnum:=ContructeurEnum.CreateType;
end;

L'enregistrement sur disque se fait de la façon suivante :

 
Sélectionnez

 If Not (AssemblyAcces=AssemblyBuilderAccess.Run) // L'assembly doit pouvoir être enregistré (persistant)
   then ConstructeurAssembly.Save(cstAssemblage+'.dll');

L'objectif étant de manipuler cette énumération générée, on crée une instance de la classe par l'appel de la méthode Activator.CreateInstance :

 
Sélectionnez

var  I        : Integer;
     MonEnum,
     UneEnum  : System.Enum;
begin
 ...
  // Manipule l'enumération créée
  If Not (AssemblyAcces=AssemblyBuilderAccess.Save) // L'assembly doit pouvoir être exécuté
   then
    begin
     MonEnum:=System.Enum(Activator.CreateInstance(TypeDynEnum));

     Writeln('Contenu de l''énumération crée :');
     for UneEnum in Enum.GetValues(typeof(MonEnum)) do
      begin
       WriteLn(TObject(UneEnum));
      end;
      // Ou pour ce contexte, directement
    { for UneEnum in Enum.GetValues(TypeDynEnum) do
    }
   end
  else 
   Writeln('L''énumération dynamique ne peut être instanciée avec le mode d''accès : ',TObject(AssemblyAcces));

A partir du moment où "Les identificateurs d'une énumération sont considérés comme des constantes du type énuméré", les instructions Ord, Succ, Pred, Low, High ne pourront être utilisées sur des 'constantes' d'une énumération dynamique pour la simple raison qu'elles sont inconnues du compilateur.

Je vous invite à consulter les autres classes 'Builder', sachez que certaines opérations s'avèreront un peu plus complexes à mettre en œuvre notamment la création du code d'une méthode de classe ...


Voir :
La classe EnumBuilder.
System.Reflection.Emit, espace de nom contenant les classes 'builder'.

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 Laurent Dardenne. 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.