Cours Delphi .NET La classe System.Type et les génériques, tutoriel & guide de travaux pratiques Delphi en pdf.
A quoi servent-ils
Prenons le cas de figure d’une structure de données telle qu’une pile, elle peut contenir des entiers, des objets ou tout autre type reconnus.
Aujourd’hui sous Win32 ou dotNET 1.1 il nous faut déclarer un typeTStack pour chaque type de donnée existant ou tout du moins utilisé dans nos traitements. Avec Delphi 2007 et uniquement sous dotNET 2.0 il est désormais possible de définir une fois pour toute une classe TStack générique, ce qui nous permettra de préciser lors de son utilisation quel type on souhaite manipuler. La généricité nous offrant ici la construction de type paramétrable et permet donc de faire abstraction lors de la conception des types de données manipulés.
Sous .NET 2.0 l’usage des génériques supprime leboxing/unboxing et les opérations de transtypage entre les types de données, limitant ainsi les vérifications de type lors de l’exécution du programme.
Voir sur le forum Delphi cet exemple propice à l’usage de type générique.
Voici un extrait commenté de l’unitéActnMenus :
//Pile spécialisée afin de manipuler le type TCustomActionMenuBar;
TMenuStack = class(TStack)
…
function TMenuStack.GetBars(const Index: Integer): TCustomActionMenuBar;
begin
//Transtypage obligatoire car
//l’appel de TList.Get renvoi un objet.
Result := TCustomActionMenuBar(List[Index]);
end;
//La pile est construite autour d’une liste d’objet
function TList.Get(Index: Integer): TObject;
begin
Result := FList[Index];
end;
//FList étant déclarée ainsi :
property List: System.Collections.ArrayList read FList;
Le framework 2.0 propose dans l’espace de nom System.Collections.Generic des collections et des interfaces génériques.
Comment ça fonctionne ?
Projet : ..\PremierGenerique
Etudions la déclaration d’une classe générique :
type
TGenericType<AnyType> = class
FData: AnyType;
function GetData: AnyType;
procedure SetData(Value: AnyType);
property Data: AnyType read GetData write SetData;
end;
function TGenericType<AnyType>.GetData: AnyType;
begin
Result := FData;
end;
procedure TGenericType<AnyType>.SetData(Value: AnyType);
begin
FData := Value;
end;
Ici TGenericType<AnyType> indique une classe générique, AnyType étant le paramètre de type référençant un type de donnée tel qu’Integer ou String. Chaque occurrence de AnyType présente dans cette déclaration sera, lors de la construction du type à la compilation, substituée par l’argument de type que l’on précisera de cette manière :
type
TGenericTypeInt = TGenericType<Integer>; //Déclare un type à partir d’une classe générique
TGenericType<Integer> est appelé un type construit fermé . Integer étant appelé ici un argument de type pour AnyType.
TGenericType<AnyType> étant quant à lui un type construit ouvert tout comme TGenericAutre<V,String>, car V est un type indéterminé.
Notez que le nom du paramètre de type, utilisé dans une déclaration de générique, ne défini pas l’unicité du type :
type
TGenericType<AnyType> = class
FData: AnyType;
end;
TGenericType<ParametreDeType> = class //Erreur à la compilation
FData: ParametreDeType;
end;
TGenericType<AnyType,N> = class //Surcharge Autorisée
FData1: AnyType;
FData2: N;
end;
La présence de la classe générique TGenericType<ParametreDeType> provoquera, à la compilation, l’erreur suivante :
E2037 : La déclaration de ‘TGenericType<AnyType>’ diffère de la déclaration précédente
Vous remarquerez qu’une déclaration de générique peut contenir plusieurs paramètres de type séparés par une virgule. Comme par exemple la classe du framework 2.0 Dictionary.
Les limites actuelles sous Delphi :
– le débogueur n’évalue pas les génériques.
– le refactoring ne prend pas en charge les génériques.
– error insight ne reconnait pas les génériques.
Un exemple détaillé
Après avoir déclaré notre classe et ses membres, voyons la création d’une instance de notre classe paramétrée.
Essayons la déclaration de variable suivante :
var
I: TGenericType;
ici la compilation échoue en provoquant l’erreur :
Identificateur non déclaré : ‘ TGenericType ‘ (E2003).
Pour instancier un générique il nous faut au préalable déclarer un type construit fermé qui précise l’usage de notre classe paramétrable :
type
TGenericTypeInt = TGenericType<Integer>; // La classe générique est paramétrée avec le type Integer
Ensuite l’appel de son constructeur reste classique :
var
I: TGenericTypeInt;
begin
I := TGenericTypeInt.Create;
Les constructions suivantes restent possibles :
var X: TGenericType<Integer>;
begin
X := TGenericType<Integer>.Create;
X.Data := 100;
With TGenericType<Integer>.Create do
Data := 100;
Sachez que les variables X et I sont compatibles en affectation.
Construisons un second type à partir de notre classe générique :
Projet : ..\PremierGenerique2
type
TGenericTypeString = TGenericType<string>;
var
I: TGenericTypeInt;
D: TGenericTypeString;
begin
I := TGenericTypeInt.Create;
I.Data := 100;
WriteLn(I.Data);
D := TGenericTypeString.Create;
D.Data := ‘Generic’;
WriteLn(D.Data);
I:=D; //Erreur à la compilation
La dernière affectation, I:=D, provoque l’erreur suivante :
E2010 Types incompatibles : ‘TGenericType<System.Int32>’ et ‘TGenericType<System.String>’
L’affectation entre différents types, construits à partir d’une même classe générique, n’est pas possible cela est dû au typage fort des génériques.
Les types de bases supportant les génériques
Certains types peuvent être à la base d’une déclaration de générique, en voici la liste.
Les classes
Le type class, de type référence, pourra être dérivé.
//Classe générique
TGenericClass<C>=Class
FData: T;
End;
De plus, n’importe quelle classe imbriquée dans une déclaration générique de classe ou une déclaration générique de record est elle-même une déclaration générique de classe, puisque le paramètre de type pour le type contenant doit être assuré pour créer un type construit.
Les exceptions étant des classes il est tout à fait possible de créer des exceptions génériques.
Les enregistrements
Le type record, de type valeur, ne pourra être dérivé car il ne supporte pas l’héritage.
//Record générique
TGenericEnregistrement<T>=Record
Data: T;
End;
Les tableaux
La définition de tableaux permet l’usage de paramètre de type.
type
TGeneriqueArray<X> = array of X;
TGeneriqueArray2D<Y> = array of TGeneriqueArray<Y>; // Equivalent à Array of Array of Y
//Tableau à une dimension contenant des chaînes de caractères TGtabString = TGeneriqueArray<String>;
//Tableau à deux dimensions contenant des entiers
TGtab2DInt = TGeneriqueArray2D<Integer>;
//Types compatibles
TGtabInteger=TGeneriqueArray<Integer>;
TGtabIntegerV2 = array of Integer;
Notez que les deux derniers types sont compatibles.
Les procédures et fonctions
Projet : ..\ProcedureEtFonction
Les paramètres et le type de retour d’une fonction peuvent être des paramètres de type.
type
TProcedureGenerique<A> = procedure(Param1 : A);
TProcObjetGenerique<B> = procedure(X,Y: B) of object; //Méthode d’objet
TFonctionGenerique<T> = Function : T;
TMaClasse = class
procedure UneMethode<T>(X, Y: T); // Même signature que le type TProcObjetGenerique<B> procedure TestMethode;
procedure TestProcedure<UnType>(Prc:TProcedureGenerique<UnType>); procedure TestFonction<T>(fnct: TFonctionGenerique<T>);
end;
L’usage d’un paramètre de type dans la signature de la procédureTestProcedure permet de le propager afin de déclarer le type de l’argument nomméPrc. Sinon la présence seul d’un argument de type dans la signature d’une méthode
TMaClasse = class
…
procedure TestProcedure(Prc:TProcedureGenerique<UnType>);
end;
signalerait un identificateur inconnu pour UnType.
Ici on pourra donc passer en paramètre n’importe quelle procédure de typeTProcedureGenerique<A>.
Voici un exemple d’appel :
Procedure ProcedureGeneriqueInt(M:Integer);
begin
Writeln(M);
end;
Procedure ProcedureGeneriqueString(M:String);
begin
Writeln(M);
end;
..
With TMaClasse.Create do
begin
TestProcedure<Integer>(ProcedureGeneriqueInt);
//TestProcedure<Integer>(ProcedureGeneriqueString); //E2010 Types incompatibles : ‘Integer’ et ‘string’
//TestProcedure<String>(ProcedureGeneriqueInt); //E2010 Types incompatibles : ‘string’ et ‘Integer’
TestProcedure<String>(ProcedureGeneriqueString);
end;
Notez que l’on doit préciser un argument de type pour ce type d’appel.
La construction suivante Procedure ProcedureGenerique<A>(M:A); provoque l’erreur suivante à la compilation :
E2530 Les paramètres de type ne sont pas autorisés sur la fonction ou la procédure globale.
Ce qui signifie que l’usage d’un type construit ouvert ne pourra donc se faire qu’au travers d’un membre d’une classe ou d’un record.
Voyons maintenant l’appel d’une méthode :
procedure TMaClasse.UneMethode<T>(X, Y: T);
begin
Writeln(X.ToString,’ , ‘,Y.ToString);
end;
procedure TMaClasse.TestMethode;
var
P: TProcObjetGenerique<Boolean>; //Argument de type Boolean connu
begin
UneMethode<String>(‘Hello’, ‘World’); //Argument de type String connu UneMethode(‘Hello’, ‘World’);
UneMethode<Integer>(10, 20); //Argument de type Integer connu
UneMethode(10, 20);
P:=UneMethode<Boolean>;
P(False,True);
end;
Les différents arguments type de notre méthode générique étant précisés dans le corps de cette méthode de test, sa manipulation ne pose pas de problème particulier.
Le mot clé Default
Si vous avez chargé et exécuté le code du projet précédent vous aurez noté le déclenchement d’une exception NullReferenceException lors de l’appel de procédureTestProcedure<String> ainsi que l’usage du mot cléDefault.
Regardons de plus près cette instruction.
procedure TMaClasse.TestProcedure<UnType>(Prc:TProcedureGenerique<UnType>);
var
P: TProcedureGenerique<UnType>; Value: UnType;
begin
//Le type est déterminé dans la signature, il est donc inconnu.
Prc(‘Hello’);
P:=Prc;
//Value:=’Chaine’; //E2010 Types incompatibles : ‘UnType’ et ‘string’ Value:=Default(UnType);
P(True);
P(Default(UnType));
end;
Dans la signature et le corps de la procédure générique précédente, le type réel de UnType est inconnu au moment de l’écriture de ce code. On ne peut donc assigner une valeur quelconque à la variableValue.
Default renvoi la valeur par défaut du type, précisé par l’argument type utilisé. Pour le typeInteger Default renverra 0.
Integer étant un type valeur, la valeur par défaut est dans ce cas connue mais si le type utilisé est un type référence, comme un TObjet ou une String, la valeur renvoyée sera égale à Nil.
On testera donc, avant toute manipulation, le contenu des variables de type référence en utilisant l’instruction Assigned.
if assigned(TObject(Value)) //Le cast est obligatoire
then writeln(‘Assigné.’)
else writeln(‘Non assigné.’);
Le type étant inconnu lors de la compilation la manipulation de la variable Value nécessite un transtypage en TObject et donc une opération de boxing.
Les méthodes
Projet : ..\Methodes1
Revenons sur la manipulation des méthodes génériques, dans la procédure TMaClasse.TestMethode vous aurez remarqué 2 types d’appel pourUneMethode<String> :
UneMethode<String>(‘Hello’, ‘World’);
UneMethode(‘Hello’, ‘World’);
Le second appel ne précise pas l’argument de type car on utilisel’inférence de typequi n’est autre que la déduction de l’argument de type par le compilateur d’après le type de donnée des arguments utilisés lors de l’appel. L’inférence reste possible tant qu’il n’y a pas d’ambigüité sur les types de donnée utilisés.
Comme nous l’avons vue, il est tout à fait possible de construire une classe et tous ses membres en utilisant des arguments de types.
TGenericType<AnyType> = class
FData: AnyType;
function GetData: AnyType;
procedure UneMethodeGenerique<AnyType>(Variable: AnyType);
function UneFonctionGenerique<AnyType>: AnyType;
procedure SetData(Value: AnyType);
property Data: AnyType read GetData write SetData;
end;
Cette construction génère l’avertissement suivant :
H2509 Identificateur ‘AnyType’ en conflit avec les paramètres type du type conteneur
car l’usage, dans la déclaration de la méthodeUneMethodeGenerique, d’un paramètre type portant le même nom, AnyType, masque celui de la classe. On ne pourra donc pas accéder dans cette méthode au paramètre type de la classe. Si seul le type de l’argument nomméVariable doit suivre celui indiqué lors de la construction de la classe générique vous pouvez omettre le paramètre de type :
TGenericType<AnyType> = class
FData: AnyType;
function GetData: AnyType;
procedure UneMethodeGenerique(Variable: AnyType);
function UneFonctionGenerique: AnyType;
procedure SetData(Value: AnyType);
property Data: AnyType read GetData write SetData;
end;
Sinon utiliser un autre nom pour le paramètre de type :
procedure UneMethodeGenerique<T>(Variable: T);
Notez que l’ajout des déclarations suivantes forcerait l’usage de la directiveOverload sur la méthode
UneMethodeGenerique :
procedure UneMethodeGenerique(Variable: AnyType);Overlaod;
procedure UneMethodeGenerique<R>(Variable: AnyType; Variable2: R);Overload;
procedure UneMethodeGenerique<T,U>(Variable: U; Variable2: T);Overload;
Les délégués
Comme les méthodes, les délégués permettent d’utiliser des gestionnaires d’événements génériques.
TOnMonEvenement<T> = procedure (Sender: TObject; var Valeur: T) of object;
TMaClasse<T>=class
private
FOnMonEvenement: TOnMonEvenement<T>;
public
property OnMonEvenement: TOnMonEvenement<T> read FOnMonEvenement write FOnMonEvenement;
end;
Les interfaces
Projet : ..\Interfacegeneric
La définition d’interface permet l’usage de paramètre de type.
type
IMonInterface<T>= interface
procedure set_Valeur(const AValeur: T);
function get_Valeur: T;
property Valeur: T read get_Valeur write set_Valeur;
end;
IMonInterfaceDerivee<T>= interface(IMonInterface<T>)
Procedure Multiplier(AMulplicateur:T);
end;
A la compilation on verra que la procédure Multiplier ne peut être implémenté :
Procedure TClasseTest<T>.Multiplier(AMulplicateur:T);
begin
//E2015 Opérateur non applicable à ce type d’opérande
FCompteur:=FCompteur * AMulplicateur;
end;
Comme on ne connait rien du type T certaines opérations ne seront pas possible avec les génériques, comme ici l’utilisation de l’opérateur de multiplication.
Pour information
Les énumérations ne peuvent pas être génériques. Enumerations in the Common Type System :
« It is possible to declare a generic enumeration in Microsoft intermediate language (MSIL) assembly language, but a TypeLoadException occurs if an attempt is made to use the enumeration. »
Ensemble<T>=Set of T; //Erreur
Projet : ..\ClasseHelper
Les assistants de classe ne sont pas autorisés avec les types construits ouverts, le code suivant ne compile pas :
TGenerique<T> = class
Champ: T
end;
THelperGenerique<T>=Class Helper for TGenerique<T>
End;
Erreur : E2508 Les paramètres de type ne sont pas autorisés sur ce type
En revanche il reste possible de les utiliser sur des types construits fermés :
TGeneriqueInt=TGenerique<Integer>;
THelperGeneriqueInt=Class Helper for TGeneriqueInt
Procedure Test;
End;
Contraintes
Lors de la construction d’un type ou d’une méthode générique on peut contraindre un argument de type à respecter certaines règles.
Par exemple dans le cas où une classe générique utilise dans son code un itérateur IEnumérable on doit s’assurer que le type réel de l’argument de type rempli bien ce contrat. La présence de cette contrainte autorisera par là même la compilation du code utilisant cette interface.
Les mots clés class, constructor et record permettront d’en spécifier une ou plusieurs. Notez que la construction suivante ne stipule aucune contrainte :
TGenericType<T>
Les mots clés class et record sont des contraintes exclusives.
Sur une classe ancêtre
On contraint l’argument de typeT à dériver d’une classe ancêtre particulière, ici de la classe TComponent :
TGenericType<T:TComponent>
Sur une ou plusieurs interfaces
On contraint l’argument de type à respecter le contrat d’une ou plusieurs interfaces génériques ou non.
L’exemple suivant contraint l’argument de typeT à implémenter l’interface générique IMonInterface<T>
TGenericType<T:IMonInterface<T>>
Sur l’accès à un constructeur sans paramètre
Projet : ..\ContrainteConstructeur
On utilisera le mot clé constructor pour contraindre un argument de type à posséder un constructeur sans paramètre et d’accès public.
On s’assure ainsi de pouvoir créer dans le corps des méthodes une instance du type passé en paramètre.
1 – Public concerné
1-1 – Les sources
2 – Les génériques qu’est-ce que c’est ?
3 – A quoi servent-ils
4 – Comment ça fonctionne ?
5 – Un exemple détaillé
6 – Les types de bases supportant les génériques
6-1 – Les classes
6-2 – Les enregistrements
6-3 – Les tableaux
6-4 – Les procédures et fonctions
6-4-1 – Le mot clé Default
6-5 – Les méthodes
6-6 – Les délégués
6-7 – Les interfaces
6-8 – Pour information
7 – Contraintes
7-1 – Sur une classe ancêtre
7-2 – Sur une ou plusieurs interfaces
7-3 – Sur l’accès à un constructeur sans paramètre
7-4 – Sur un type référence
7-5 – Sur un type valeur
7-6 – Sur un type nu (naked type)
8 – Héritage
8-1 – Portée des paramètres de type
9 – Surcharge de classe
9-1 – Surcharge d’opérateur
10 – Variable de classe dans les génériques
11 – La classe System.Type et les génériques
12 – L’instanciation des types génériques
13 – Type Nullable
13-1 – La déclaration
13-2 – L’assignation
13-3 – Opérateurs
13-4 – Diverses manipulations
14 – Les opérateurs is et as
15 – Classes partielles
16 – Net 3.0 et WPF
17 – Liens