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.
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 :
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 :
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 :
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 EtatCourantbegincase EtatCourant of
Bleu : Result:='Bleu';
Blanc : Result:='Blanc';
Noir : Result:='Noir';
Rouge : Result:='Rouge';
Vert : Result:='Vert';
end;
end;
Var uneCouleur : Couleurs;
I: Byte;
beginWrite('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éroWrite('Liste ordonnée des valeurs : ');
For I:=byte(Low(Couleurs)) to Byte(High(Couleurs)) dobeginWrite(GetCouleur(Couleurs(I)));
if I<>Byte(High(Couleurs))
thenWrite(', ');
end;
Readln;
end.
Projet : ..\Enum\BasesEnumeration2
La fonction GetCouleur n'est plus nécessaire si on utilise les fonctionnalités objet du framework :
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 :
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 LONGenum 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 chaînes 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 chaînes de caractèresfor Chaine in Enum.GetNames(typeof(Couleurs)) dobegin
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'objetsfor UneBoite in Enum.GetValues(typeof(Couleurs)) dobegin
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 :
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érationfor Chaine in Enum.GetNames(typeof(Environment.SpecialFolder)) dobeginWrite(Chaine+' , ');
end;
// Test de valeurif 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;
beginWith System.IO.Filedobegin
flagAttr := GetAttributes(NomFichier);
for UnAttr in Enum.GetValues(TypeOf(System.IO.FileAttributes)) doif ( (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 :
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 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 :
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) Andnot 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 dobeginif 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 :
Pour maîtriser la génération de code à la volée, il convient de bien connaître la structure dun assemblage.
Pour rappel, un assemblage est constitué dun ou de plusieurs modules (cest-à-dire un assemblage de
NetModules). Chaque module dispose dune 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'assemblyvar 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 seffectue 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 dynamiquebegin
...
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 Dynamiquebegin
...
// Créer un module dynamique persistantcase 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érationbegin
...
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.
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 :
IfNot (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ééeIfNot (AssemblyAcces=AssemblyBuilderAccess.Save) // L'assembly doit pouvoir être exécutéthenbegin
MonEnum:=System.Enum(Activator.CreateInstance(TypeDynEnum));
Writeln('Contenu de l''énumération crée :');
for UneEnum in Enum.GetValues(typeof(MonEnum)) dobegin
WriteLn(TObject(UneEnum));
end;
// Ou pour ce contexte, directement{ for UneEnum in Enum.GetValues(TypeDynEnum) do
}endelse
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 ...