Les énumérations sous Delphi .NET

Laurent Dardenne

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

- Titre : Les énumérations sous Delphi .NET

- Auteur : Laurent Dardenne

- Parution : 1 juillet 2005

- Mise à jour : 1 juillet 2005

- Licence : 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.

Sommaire

- I. Public concerné

- I-A. Les sources

- II. Les bases

- III. Une nouvelle approche

- IV. Manipulation des énumérations du framework

- V. Du nouveau : les champs de bits

- VI. Construction dynamique d'une énumération avec EnumBuilder

I - Public concerné

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

I-A - Les sources

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

II - Les bases

Projet : ..\Enum\BasesEnumeration

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

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 a 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 :

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

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

Un exemple de leur utilisation :

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 :

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

III - 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 :

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.

// 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 :

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 :

//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 chaines des caractères représentant les constantes nommées :

var Chaine : String;
begin
// Affiche les valeurs possibles de l'énumération
// Récupère un tableau de chaines 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 :

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;

IV - 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 :

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 :

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 :

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;

V - 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 :

[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) :

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 :

//Affectation via une chaine 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 :

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 :

// 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 :

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 :

Resultat = All

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

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.

VI - 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 :

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 :

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 :

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éé :

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éé :

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 :

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 :

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

provoque l'exception ArgumentException :

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

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

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

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 :

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));

À 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'.