Les références de classe ou métaclasses

Laurent Dardenne

Cet article est issu d'une discussion sur les métaclasses sur le forum de la FAQ Delphi de Developpez.com.

Les méthodes de classes appelées parfois métaclasses et les références de classe sont des éléments du langage important sous Delphi. L'aide en ligne n'étant pas très prolixe sur le sujet (bien que de nombreux extraits émaillent mes propos) et n'ayant trouvé sur le net que des bribes d'information, cet article vous propose d'approfondir le sujet, enfin je l'espère.

Merci à Ptitbob, Nono40, Pierre Castelain, Hachesse, Bloon et Delphiprog. ?

- Titre : Les références de classe ou métaclasses

- Auteur : Laurent Dardenne

- Parution : 29 juillet 2004

- Public visé :

- Licence : Ce document est issu de www.developpez.com et reste la propriété exclusive de son auteur. La licence d'utilisation Open Content (http://opencontent.org/opl.shtml) vous permet d'utiliser librement les documents publiés sur mon espace personnel à condition de mentionner clairement le nom de son auteur avec un lien sur mon espace personnel de ce site et de garder son caractère « Â Open ContentÂ ».

Sommaire

- I. Avant-propos

- II. Définitions

- II-A. Classe

- II-B. Méthode

- II-C. Objet, instance ou instance de classe

- II-D. Variable ou référence d'objet

- III. Les méthodes abstraites

- III-A. Exception EAbstractError

- IV. Les prédéclarations de classes

- V. Les pointeurs de méthode d'objet

- VI. Les références de classe ou métaclasses

- VI-A. Affectation et variable de classe

- VI-B. Constructeur virtuel

- VI-C. Le polymorphisme

- VI-D. Étude de cas

- VI-D-1. Gestion des trois classes souhaitées

- VI-D-2. Ajout de la classe TObject

- VI-D-3. Ajout d'une classe ascendante

- VII. Les méthodes de classe

- VII-A. Le pattern Singleton

- VII-B. Retrouver une classe par son nom

I - Avant-propos

Testé avec Delphi 5 sous XP pro.

II - Définitions

II-A - Classe

Une classe (un type classe) définit une structure composée de champs, de méthodes et de propriétés. Les champs, méthodes et propriétés d'une classe sont appelés ses composants ou ses membres.

type NomDeClasse = class (ClasseAncêtre)
ListeMembre
end;

Détails sur la déclaration de classes

II-B - Méthode

Une méthode est une procédure ou une fonction associée à une classe.

II-C - Objet, instance ou instance de classe

Les objets sont des blocs de mémoire alloués dynamiquement dont la structure est déterminée par leur type de classe. Chaque objet détient une copie unique de chaque champ défini dans la classe. Par contre, toutes les instances d'une classe partagent les mêmes méthodes. Les instances d'un type classe, c'est-à-dire les instances de classe, sont appelées des objets.

II-D - Variable ou référence d'objet

Une variable de type classe est en fait un pointeur qui référence le bloc mémoire alloué pour un objet. Plusieurs variables peuvent donc désigner le même objet. Comme les autres pointeurs, les variables de type classe peuvent contenir la valeur nil, dans ce cas elle ne fait donc aucune référence à un objet.

Une variable de type TObject contient une instance de la classe TObject.

Note : Un constructeur renvoie une référence d'objet.

III - Les méthodes abstraites

Abstrait
«  Qui use d'abstractions opère sur des qualités et des relations et non sur la réalité. »

Une méthode abstraite est une méthode virtuelle ou dynamique n'ayant pas d'implémentation dans la classe où elle est déclarée. Son implémentation est déléguée à une classe dérivée. Les méthodes abstraites doivent être déclarées en spécifiant la directive abstract après virtual ou dynamic.

Par exemple :

procedure FaireQuelquechose; virtual; abstract;

Vous ne pouvez appeler une méthode abstraite que dans une classe ou une instance de classe dans laquelle la méthode a été surchargée (mot clé override ).

Voir par exemple l'utilisation d'abstract dans le source suivant livré avec Delphi

1. ..\Delphi\Demos\Threads\SortThds.pas

III-A - Exception EAbstractError

C'est la classe des exceptions pour les appels de méthodes abstraites, déclarée dans l'unité sysutils.pas

...
EAbstractError = class(Exception);
...

EAbstractError est déclenchée quand une application tente d'appeler une méthode abstraite. Elle est également déclenchée à la conception quand un composant contenant une méthode abstraite est placé sur une fiche.

Cette gestion est interne à Delphi. Nous ne pourrons donc la manipuler que dans un bloc d'exception.
Le code suivant est compilé sans warning, car aucune des méthodes utilisées n'est abstraite, mais provoquera une exception à l'exécution :

var Liste:TStrings;
Chaine:String;
begin
Try
Liste:=TStrings.Create;
Try
// La méthode Add appelle la méthode abstraite GetCount
Liste.Add('Une chaine');
Except
On EAbstractError do
MessageDlg('Vous devez implémenter la méthode GetCount', mtError, [mbOK], 0);
End ;
Finally
Liste.Free;
end;
end;

Vous pouvez créer une instance de classe possédant une méthode abstraite dans ce cas le compilateur vous avertira par le message :

175. Construction d'instance de '<...>' contenant des méthodes abstraites.

La seule chose à respecter est de ne pas appeler la ou les méthodes abstraites directement ou indirectement.

Détails sur la déclaration Abstract

IV - Les prédéclarations de classes

Le rôle d'une prédéclaration de classe est similaire au mot clé forward utilisé avec les procédures et les fonctions. Cela permet à d'autres classes ou méthodes d'appeler une classe déclarée avant qu'elle ne soit effectivement définie.

Les prédéclarations sont parfois indispensables dans le cas de déclarations croisées entre deux classes.

Déclarations partielles, extraites de l'unité Classes de la VCL :

…
{ Forward class declarations }

TStream = class;
TFiler = class;
TReader = class;
TWriter = class;
TComponent = class;
…

Ici par exemple ces prédéclarations permettent d'utiliser la classe TFiler dans un entête de méthode, sans avoir à spécifier la totalité de la déclaration de la classe.

Toujours dans l'unité Classes de la VCL :

TPersistent = class(TObject)
…
protected
procedure DefineProperties(Filer: TFiler); virtual;
…
// Et plus loin dans le code de l'unité la déclaration complète de la classe TFiler
{ TFiler }
…
TFiler = class(TObject)
private
FStream: TStream;
FBuffer: Pointer;
…

Une prédéclaration de classe suffit pour utiliser une référence de classe.

...
TComponent = class;
TComponentClass = class of TComponent;

Le compilateur accepte la syntaxe suivante, mais considérera la déclaration complète de classe plus avant dans le code comme une redéclaration de classe.

...
// Prédéclaration erronée
TUneClasseDeBase = class(TObject);

V - Les pointeurs de méthode d'objet

Pour pointer une méthode d'une instance d'objet, vous devez ajouter les mots of object au nom de type.

Par exemple les types suivants sont des pointeurs de méthode :

type

TMethode = procedure of object;

TNotifyEvent = procedure(Sender: TObject) of object;

Un pointeur de méthode est en fait une paire de pointeurs, le premier stocke l'adresse d'une méthode et le second une référence à l'objet auquel appartient la méthode.

Étant donné les déclarations :

type

TNotifyEvent = procedure(Sender: TObject) of object;
TMainForm = class(TForm)
procedure ButtonClick(Sender: TObject);
...
end;
var
MainForm: TMainForm;
OnClick: TNotifyEvent

Vous pouvez utiliser l'affectation suivante :

OnClick := MainForm.ButtonClick;

Les déclarations suivantes sont possibles :

type

Fabrique = Function ():Byte of Object;

Fabrique = Function (Reference : TControlClass; MonParent : TWinControl): TControl of Object;

Le code suivant a été proposé par Bloon.

Dans la partie published d'une Form, ajoutez une méthode nommée Fermeture qui se contente d'appeler la méthode Close.

Ensuite, utilisez l'enregistrement TMethod déclaré dans l'unité system :

type
methode = procedure of object;

procedure TForm1.Button1Click(Sender: TObject);
var
m : TMethod;
begin
m.Data := self;
m.Code := self.MethodAddress('Fermeture');
methode(m);
end;

Pour passer un paramètre :

type
methode = procedure (s : string) of object;

procedure TForm1.Button1Click(Sender: TObject);
var
m : TMethod;
begin
m.Data := self;
m.Code := self.MethodAddress('Fermeture');
methode(m)('Enregistre');
end;

Extrait de l'aide en ligne de Delphi :

TMethod est un enregistrement utilisé pour stocker les champs Code et Data comme étant du type Pointer.

type   TMethod = record
Code, Data: Pointer;
end;

VI - Les références de classe ou métaclasses

Parfois des opérations sont effectuées sur la classe même et pas sur une instance de la classe (c'est-à-dire sur un objet).

Vous pouvez toujours désigner une classe spécifique par son nom, mais dans certains cas il est nécessaire de déclarer des variables ou des paramètres qui prennent des classes pour valeur. Il faut alors utiliser des types référence de classe appelées parfois métaclasses, c'est-à-dire une classe regroupant d'autres classes dérivées de la même classe ancêtre.

Une référence de classe indique un pointeur sur une zone mémoire contenant la description de la classe.

Un type référence de classe est désigné par une construction de la forme :

class of type

où type est un type classe.

La ligne suivante déclare une référence de classe appelée TClass référençant la classe TObject.

type TClass = class of TObject;

Et ici nous déclarons une variable appelée ToutObjet qui peut contenir une référence sur n'importe quelle classe.

var ToutObjet : TClass;

Une référence de classe est un type prédéfini du langage, voici un extrait de la grammaire du Pascal Objet :

...
Type -> IdentType
-> TypeSimple
-> TypeStruc
-> TypePointeur
-> TypeChaîne
-> TypeProcédure
-> TypeVariant
-> TypeRefClass

TypeRestreint -> TypeObjet   {* Borland recommande ne plus utiliser ce type pour la programmation objet}
-> TypeClasse
-> TypeInterface

TypeRefClass -> CLASS OF IdentType
...

Les types référence de classe sont utiles quand vous voulez désigner une méthode de classe ou un constructeur virtuel pour une classe ou un objet dont le type réel est inconnu à la compilation.

Les références de classes sont utilisées dans les situations suivantes :

- avec un constructeur virtuel pour créer un objet dont le type réel est inconnu au moment de la compilation ;

- avec une méthode de classe pour effectuer une opération sur une classe dont le type réel est inconnu au moment de la compilation ;

- comme opérande droit d'un opérateur is en vue d'accomplir un transtypage contrôlé lorsque le type est indéterminé au moment de la compilation ;

- comme opérande droit d'un opérateur as en vue d'accomplir un transtypage contrôlé vers un type indéterminé au moment de la compilation.

La définition d'un type référence de classe ne peut pas se faire directement dans une déclaration de variable ou dans une liste de paramètres.

// Déclaration impossible
Var TClass : class of TObject;

// Déclaration impossible
Function GetSize(Classe : class of TObject ):Integer ;

Rappel

1. Quand la déclaration d'un type classe ne spécifie pas d'ancêtre, la classe hérite directement de TObject.

Donc la déclaration suivante :

type TMaClasse = class
...
end;

est équivalente à :

type TMaClasse = class(TObject)
...
end;

Borland recommande cette dernière forme dans un souci de lisibilité.

De plus l'utilisation de la déclaration d'objet suivante, héritée du Turbo Pascal, est proposée dans un souci de compatibilité ascendante, mais son utilisation est déconseillée :

type
nomTypeObjet = object(typeObjetAncêtre)
listeMembre
end;

VI-A - Affectation et variable de classe

Une valeur de type référence de classe est compatible en terme d'affectation avec tout type référence de classe ancêtre.

Extrait de l'unité Classes de la VCL :

…
{ TComponent class reference type }

TComponentClass = class of TComponent;
…
TComponent = class(TPersistent)
Private
// Déclaration complète de la classe
…
TControl = class(TComponent)
Private
// Déclaration complète de la classe
…
TControlClass = class of TControl;

Par exemple pour les variables suivantes ces affectations sont valides :

Var
ComponentClass : TComponentClass ;
ControlClass   : TControlClass

Begin
ComponentClass := TComponent ; // Valide
ComponentClass := TControl; // Valide
...

Alors que dans ces affectations seules la seconde est valide :

Var
ComponentClass : TComponentClass ;
ControlClass   : TControlClass

Begin
ControlClass := TComponent ; // Invalide
ControlClass := TControl; // Valide

La première est invalide, car la classe TComponent n'est pas une classe descendante de TControl et n'est donc pas une valeur de type TControlClass.

De même lors d'une vérification de type on doit vérifier des classes descendantes vers les classes ancêtre, car une instance de la classe descendante est aussi une instance de la classe ancêtre.

Si on souhaite connaître la classe exacte d'une instance de classe afin d'effectuer un traitement particulier :

if Instance is TControl
then …
else
if Instance is TComponent
then …
else
if Instance is TPersistent
then …
else
if Instance is TObject
then …

Une variable de type référence de classe peut contenir la valeur Nil, dans ce cas elle ne fait référence à aucune classe.

ControlClass   := Nil;
if ControlClass = Nil
then Exit;

En fait, c'est par l'utilisation des références de classe et des constructeurs virtuels que le concepteur de forme de Delphi peut créer les composants qui sont placés sur des fiches.

VI-B - Constructeur virtuel

Pour des explications plus détaillées, je vous renvoie vers les cours suivant au lieu de réécrire ce qui a été déjà très bien dit, nous appliquerons donc le principe de la réutilisation :-)

Cours sur les constructeurs virtuels
Héritage et notions évoluées

Quand il est appelé via un identificateur de type classe ( TEdit.Create ), un constructeur déclaré comme virtuel est équivalent à un constructeur statique. Par contre, quand ils sont combinés avec des types référence de classe ( ReferenceClass.Create ), les constructeurs virtuels permettent une construction polymorphique des objets : c'est-à-dire la construction d'objets dont le type est inconnu à la compilation.

Par exemple dans le code qui suit la variable Reference est une référence de classe pour la classe TClasseDeBase. Par conséquent, toutes les classes dérivées de TClasseDeBase peuvent être assignées à Reference.

TClasseDeBase = class( TObject )
Numero: Byte;
constructor Create; { Constructeur statique }
end;

TClasseDescendante = class( TClasseDeBase )
constructor Create; { Constructeur statique }
end;

TExemple = class( TClasseDeBase )
constructor Create; { Constructeur statique}
end;

TClassDeClasseDeBase = class of TClasseDeBase;  { Référence de classe }

var
Form1: TForm1;

Reference : TClassDeClasseDeBase;       { Variable référence de classe }
Instance  : TClasseDeBase;              { Variable d'instance de classe }

Le code de cet exemple

Afin de visualiser le comportement entre un constructeur statique et un constructeur virtuel ce code est volontairement erroné, bien que sa syntaxe soit correcte.

Exécutez ce programme et créez un objet de chaque classe.

Dans cet exemple tout fonctionne correctement en dehors du fait que le contenu de la variable Numero ne correspond pas à ce que nous attendons. C'est-à-dire que les données spécifiques aux classes descendantes ne sont pas initialisées.

Instance  := Reference.Create;
Memo1.Lines.Add('Création d''une instance de la classe '+Instance.Classname);

Le code précédant crée bien une instance de la classe voulue, mais le constructeur appelé est toujours celui de la classe ancêtre. À noter que si nous déclarons une méthode virtuelle dans chacune de ces classes, la méthode de la classe réelle est bien appelée.

Modifions les déclarations de classe comme ceci :

TClasseDeBase = class( TObject )
Numero: Byte;
constructor Create; Virtual;   { Constructeur Virtuel }
end;

TClasseDescendante = class( TClasseDeBase )
constructor Create; override; { Constructeur surchargé }
end;

TExemple = class( TClasseDeBase )
constructor Create; override;  { Constructeur surchargé }
end;

Le code modifié

L'ajout de la directive Virtual ou override pour les classes descendantes permet bien d'appeler le constructeur de la classe référencée et non pas celui de la classe ancêtre.

VI-C - Le polymorphisme

Polymorphe
«  Qui peut se présenter sous des formes différentes. »

Polymorphisme ou polymorphie
«  Caractère de ce qui est polymorphe. »

Nous avons pu voir que la référence de classe associée au constructeur virtuel permet l'implémentation du polymorphisme, c'est-à-dire permettre à une instance d'avoir plusieurs formes ou plutôt plusieurs comportements.

La référence d'objet utilisée se présentera à un moment t sous une seule forme, mais se comportera de manière différente tout au long de l'exécution du programme.

À noter qu'il ne faut pas confondre le polymorphisme avec l'héritage multiple, voir aussi le pattern Wrapper. C'est-à-dire permettre à un objet de se présenter sous une seule forme tout au long de son existence, mais pouvant se comporter de différente manière.

Détails sur le polymorphisme

https://rmdiscala.developpez.com/cours/livres/basesinfo5.3.pdf

Guide Pascal et Delphi - Polymorphisme

VI-D - Étude de cas

Nous souhaitons lors de l'exécution du programme créer une instance de TControl parmi les trois classes descendantes suivantes : TEdit,TButton,TMemo.

La classe que nous souhaitons instancier est donc inconnue au moment de la compilation. Nous utiliserons la référence de classe TControlClass déclarée dans l'unité classes qui nous permettra de manipuler les trois classes citées.

Nous utiliserons une seule variable référence de classe pouvant être tour à tour soit un TEdit, soit un TButton, soit un TMemo.

VI-D-1 - Gestion des trois classes souhaitées

Cet exemple implémente de manière simple le polymorphisme, ici ce qui va différencier chaque control c'est la manière dont il s'affiche à l'écran. Le control se comportera différemment selon sa classe réelle à l'exécution. Il existe bien sûr d'autres différences, mais l'affichage étant la plus 'remarquable'.

L'interface de notre programme permettra donc de créer une instance parmi ces trois classes.

Le bouton Créer utilisera une méthode regroupant la sélection de la classe puis la création de l'instance.

Le bouton Fabriquer utilisera une méthode dédiée à la création d'instance de classe TControlClass.

Première version du code

Exécutez le programme et créez une instance de chaque classe.

1. procedure TForm1.Creer;

2. Begin

3. FreeAndNil(Instance);

4. case rgClasseObjet.ItemIndex of

5. 0 : ControlClass:=TEdit;

6. 1 : ControlClass:=TButton;

7. 2 : ControlClass:=TMemo;

8. end;

9. // Appel la méthode de classe

10. MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);

11. // provoque une erreur lors de la compilation :

12. // MemoLog.Lines.Add('Création d''une instance de classe '+ControlClass.ClassType);

14. Instance :=ControlClass.Create(Self );

15. Instance.Parent:=Panel1;

16. end;

La ligne 3 permet de supprimer l'instance précédemment créée.

Les lignes 5, 6, 7 affectent à la variable référence de classe ControlClass le type de classe que l'on souhaite instancier.

La ligne 10 appelle la méthode de classe (que nous aborderons un peu plus tard) Classname afin d'afficher le type de la classe sélectionnée.

La ligne 12 nous démontre qu'une variable référence de classe ne peut pas effectuer d'appel sur des méthodes autres que des méthodes de classes.

Le compilateur renvoie :

77. Forme d'appel de méthode autorisée seulement pour méthodes de classe

La ligne 14 crée une instance de classe via l'appel du constructeur virtuel de la classe référencée dans la variable ControlClass. Les classes TEdit,TButton,TMemo possèdent toutes un constructeur Virtuel.

Et enfin la ligne 15 renseigne le parent du control créé permettant de l'afficher dans le panel placé à droite dans la fenêtre principale.

La fonction suivante est appelée via le code associé au bouton Fabriquer :

function TForm1.FabriqueUnControl(Reference : TControlClass; MonParent : TWinControl): TControl;
begin
MemoLog.Lines.Add('Création d''une instance de la classe '+Reference.ClassName);

Result:=Reference.Create(Self); // Propriétaire :  Il contrôle quand le composant est libéré
Result.Parent:=MonParent;        // Parent : C'est le contrôle fenêtré contenant le contrôle créé
end;

On retrouve donc tous les acteurs de la procédure Créer, mais ici on généralise l'utilisation du traitement de création d'instance. L'ajout du propriétaire du control (self) dans la liste des paramètres permettrait d'isoler cette fonction dans une unité.

Un autre exemple d'utilisation de type référence de classe est fourni par la déclaration du constructeur de TCollection, extrait de l'unité Classes de la VCL :

type TCollectionItemClass = class of TCollectionItem;
...
constructor Create(ItemClass: TCollectionItemClass);

Cette déclaration indique que pour créer un objet instance de TCollection, il faut transmettre au constructeur le nom d'une classe descendant de TCollectionItem.

VI-D-2 - Ajout de la classe TObject

Pour aller plus loin, on souhaite créer une instance de la classe TObject. Ajoutons donc dans les méthodes Creer et Fabriquer le choix de cette classe.

Seconde version du code

...
procedure TForm1.Creer;
Begin
FreeAndNil(Instance);
case rgClasseObjet.ItemIndex of
0 : ControlClass:=TEdit;
1 : ControlClass:=TButton;
2 : ControlClass:=TMemo;
3 : ControlClass:=TObject;
end;
...
procedure TForm1.Fabriquer;
Begin
FreeAndNil(Instance);
case rgClasseObjet.ItemIndex of
0 : Instance:=FabriqueUnControl(TEdit,Panel1);
1 : Instance:=FabriqueUnControl(TButton,Panel1);
2 : Instance:=FabriqueUnControl(TMemo,Panel1);
3 : Instance:=FabriqueUnControl(TObject,Panel1);
end;
end;
...

Si nous essayons de compiler ce projet, la compilation échoue sur le message d'erreur suivant :

Types incompatibles : '<TControlClass>' et '<Class Reference>'

La classe TObject n'étant pas un descendant de la classe TControl elle ne peut donc être assignée à la variable référence de classe ControlClass. Mais comme nous manipulons des classes, nous pouvons donc utiliser le transtypage.

Modifiez le code dans la méthode Créer comme ceci :

3 : ControlClass:=TControlClass(TObject);

et dans la méthode Fabriquer :

3 : Instance:=FabriqueUnControl(TControlClass(TObject),Panel1);

Troisième version du code

Exécutez le programme et créez une instance de chaque classe. Malheureusement la création d'une instance de classe TObject n'est pas possible, car à la différence de la méthode create des classes dérivées de TControl, celle de la classe TObject ne nécessite aucun paramètre.

Dans un premier nous pouvons ajouter un bloc d'exception permettant de gérer ce problème :

procedure TForm1.Creer;
Begin
FreeAndNil(Instance);
case rgClasseObjet.ItemIndex of
0 : ControlClass:=TEdit;
1 : ControlClass:=TButton;
2 : ControlClass:=TMemo;
3 : ControlClass:=TControlClass(TObject);
end;
// Appel la méthode de classe
MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);
Try
Instance :=ControlClass.Create(Self );
Instance.Parent:=Panel1;
Except
On E:EAccessViolation do
MessageDlg('La méthode create n''est pas adaptée pour la classe '+ControlClass.ClassName, mtError, [mbOK], 0);
end;
end;

Si vous exécutez le programme ainsi modifié, le code intercepte l'erreur, mais il n'est toujours pas possible de créer notre instance.

Ajoutons l'instruction de création dans le bloc d'exception :

Try
Instance :=ControlClass.Create(Self );
Instance.Parent:=Panel1;
Except
On E:EAccessViolation do
Instance :=ControlClass.Create;
end;

Personnellement ce détournement ne me plait pas trop. Une solution plus élégante serait de vérifier le type de la classe avec l'opérateur is. Malheureusement l'utilisation de l'opérateur is permettant une vérification de type dynamique n'est pas possible sur une référence de classe.

If ControlClass is TEdit
then ...

Par contre une référence de classe contient comme valeur un type de classe, on peut donc la tester de la manière suivante :

If ControlClass=TObject
then ...
Le code suivant est aussi possible :

var ControlClass : TControlClass;
ObjetClass     : TClass;

begin
ControlClass:=TEdit;
ObjetClass:= TPersistent;

If ControlClass=ObjetClass
then ...
mais pas celui-ci

If ControlClass=TClass
then ...

Par contre si nous testons la référence de classe ControlClass après lui avoir affecté la valeur TEdit ce test (If ControlClass=TObject) fonctionne toujours. Le résultat est correct, mais dans ce cas il nous faut tester les 4 classes, des classes descendantes vers la classe ancêtre. Dans notre cas présent, la possibilité de tester une branche complète d'une hiérarchie d'héritage faciliterait la lecture et l'écriture du code. La méthode de classe InheritsFrom permet ce type de vérification.

Texte issu de l'aide en ligne de Delphi :

TObject.InheritsFrom

Détermine la relation entre deux types d'objets.

class function InheritsFrom(AClass: TClass): Boolean;

Description
Utilisez la méthode InheritsFrom pour déterminer si un type de classe donné est l'ancêtre d'un objet.
InheritsFrom renvoie True si le type d'objet spécifié par le paramètre AClass est bien l'ancêtre du type de l'objet,
ou le type même de l'objet. Sinon, elle renvoie False.

Remarque :
Les opérateurs is et as utilisent dans leur implémentation la méthode InheritsFrom. Cependant, l'opérateur is ne peut
déterminer la relation d'héritage que pour une instance. Alors que InheritsFrom, comme méthode de classe, est capable
de déterminer la relation entre des références de classe.

En utilisant cette méthode on peut donc vérifier si la classe référencée hérite bien de TControl, modifions le code source de la méthode Créer et fabriquer en supprimant le bloc d'exception devenu inutile :

procedure TForm1.Creer;
Begin
FreeAndNil(Instance);
case rgClasseObjet.ItemIndex of
0 : ControlClass:=TEdit;
1 : ControlClass:=TButton;
2 : ControlClass:=TMemo;
3 : ControlClass:=TControlClass(TObject);
end;
// Appel la méthode de classe
MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);

If ControlClass.InheritsFrom(TControl)
then
begin
Instance :=ControlClass.Create(Self );
Instance.Parent:=Panel1;
end
else Instance :=ControlClass.Create;
end;

Quatrième version du code

Si vous compilez ce programme ainsi modifié, la compilation échoue. Nous devons effectuer les transtypages suivant pour pouvoir générer l'exécutable :

...
else TObject(Instance) :=TObject(ControlClass).Create;
end;

Exécutez ce programme, puis créez tour à tour une instance de chaque classe. Nous rencontrons encore le problème lors de la création d'une instance de la classe TObject.

Le transtypage de la référence d'objet Instance, en utilisant l'identificateur de type classe TObject est correct.

Le transtypage de la référence de classe ControlClass ne l'est pas, on doit utiliser l'identificateur de type référence de classe TClass.

...
else TObject(Instance) :=TClass(ControlClass).Create;
end;

Dans le premier cas, le transtypage induit en erreur le compilateur qui traite ControlClass comme une instance et non comme une référence de classe.

// Identificateur de type référence de classe
type TClass = class of TObject;

// Identificateur de type classe
type TPersistent = class(TObject);

Ce type de transtypage est nécessaire lorsque l'on souhaite récupérer le type de classe d'une instance, par exemple la méthode Classtype renvoie une valeur de référence TClass :

ControlClass:=TControlClass(Sender.ClassType);

VI-D-3 - Ajout d'une classe ascendante

Dernière version du code

Pour terminer, nous allons ajouter la création d'une instance de la classe TComponent en ajoutant la vérification de type comme nous l'avions vu précédemment.

procedure TForm1.Creer;
Begin
FreeAndNil(Instance);
case rgClasseObjet.ItemIndex of
0 : ControlClass:=TEdit;
1 : ControlClass:=TButton;
2 : ControlClass:=TMemo;
3 : ControlClass:=TControlClass(TObject);
4 : ControlClass:=TControlClass(TComponent);
end;
// Appel la méthode de classe
MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);

// Vérification d'appartenance à une Hiérarchie de classe
If ControlClass.InheritsFrom(TControl)
then
begin
Instance:=ControlClass.Create(Self );
// Vérification d'appartenance à une classe
If ControlClass=TComponent
// TComponent ne posséde pas de propriétés Parent
then Exit;
Instance.Parent:=Panel1;
end
else TObject(Instance):=TClass(ControlClass).Create;
end;

Ces exemples nous ont permis d'aborder la manipulation des méthodes de classe.

Vous trouverez dans le projet de démonstration Resxplor.dpr un exemple complet de gestion des références de classes

1. ..\Delphi\Demos\Resxplor\exeimage.pas

Vous noterez l'appel suivant dans la fonction TResourceList.List qui permet la création d'une instance sans déclaration de variable intermédiaire :

ResItem := GetResourceClass(FResType).CreateItem(Self, DirEntry);

VII - Les méthodes de classe

Nous les avons utilisées précédemment sans trop connaître leur principe. Les méthodes de classe permettent d'obtenir des informations sur une référence de classe et pas sur une instance de classe. Elles peuvent être appelées sans création d'instance de classe et leur accès peut se faire par des références d'objet ou par des références de classe comme nous avons pu le voir.

La déclaration d'une méthode de classe doit commencer par le mot clé class. Exemple de déclaration de la méthode ClassName de la classe TObject :

class function ClassName: ShortString;

La déclaration de définition d'une méthode de classe doit également commencer par le mot clé class :

class function TObject.ClassName: ShortString;

Dans le cas d'une méthode de classe l'utilisation de self à une signification différente, il s'agit d'une variable de type référence de classe contenant la classe effective de l'objet.

Self : Class of ClassType

Dans ce cas Self n'est pas une référence d'objet, vous ne devez donc pas accéder aux champs, propriétés et méthodes normales (hormis bien sûr d'autres méthodes de classe et les constructeurs).

Exemple d'utilisation :

Var UnComposant:TButton;
UneReference: TControlClass;
begin
// Bien que UnComposant ne soit pas initialisé, on peut utiliser ces méthodes de classe
ShowMessage('Référence d''objet '+UnComposant.ClassName);
UneReference:=TControlClass(UnComposant.ClassType);

ShowMessage('Référence de classe '+UneReference.ClassName);
UnComposant:=TButton.Create(Self);
end;

De telles méthodes peuvent permettre un comportement spécifique à la classe entière, par exemple un compteur de références. Les méthodes de classe peuvent être virtuelles et surchargées comme n'importe quelle autre méthode.

Code source de démonstration

VII-A - Le pattern Singleton

Un Singleton prend en charge la création d'une instance de classe et s'assure qu'il n'existera qu'une et seule instance tout au long de l'exécution d'un programme.

L'implémentation de ce pattern est un bon exemple d'utilisation des méthodes de classe.

Vous trouverez sur le site de Borland US un exemple d'implémentation du pattern Singleton.

VII-B - Retrouver une classe par son nom

Delphi ne propose pas en dehors de la méthode GetClass la possibilité d'obtenir une classe à partir d'un nom de classe contenue dans une chaine de caractères pour l'assigner à une référence de classe :

TControlClass=TEdit.Text; // Invalide

L'utilisation de la méthode GetClass permet de récupérer une classe à partir d'un nom de classe :

TControlClass=GetClass(TEdit.Text);

Par contre la classe doit être recensée par RegisterClass pour que la méthode GetClass puisse la trouver. Il faut donc que les classes recensées dérivent de la classe TPersistent.

Et dans le cas suivant :

UneClasse(GetClass(TEdit.Text)).Traitement(params)

il faut que :

- Traitement soit une méthode de classe. Sinon il n'est pas possible d'effectuer l'appel de méthode de cet exemple ;

- Traitement soit une méthode virtuelle ou dynamique. Sinon on risque de perdre l'héritage et donc de ne pas appeler la bonne méthode en fonction de la classe réelle.

La solution suivante proposée par Bloon implémente un mécanisme d'enregistrement pour toutes les classes.
Dans un premier temps nous pourrions utiliser la classe TClassList, mais la classe TStringList a les avantages suivants :

- si vous utilisez un nombre important de classes, la propriété Sorted permet d'accélérer la recherche ;

- la persistance, c'est-à-dire la possibilité d'enregistrer cette liste sur disque.

Pour enregistrer le nom d'une classe et son type :

Var mesClasses :TStringList;
begin
...
// Enregistre le nom de la classe et le type de la classe, mais pas l'objet
mesClasses.AddObject('TMaClasse',TObject(TMaClasse));
...

Pour retrouver une classe par son nom :

Var mesClasses : TStringList;
MaClasse   : TControlClass;
i            : Integer;

begin
...
// Recherche dans la liste d'association
i := mesClasses.IndexOf('TEdit');

if (i = -1)
// si pas trouvé, on renvoie nil
then MaClasse := nil;
// si trouvé, on renvoie la classe associée
else MaClasse:= TClass(mesClasses.objects[i]);
...

En utilisant les méthodes de classe, il est également possible de faire :

procedure addClass(UneClasse : TClass);
begin
mesClasses.AddObject(UneClasse.ClassName,TObject(UneClasse));
end;

Exemple :

type
TRefMesClasses = class of TMesClasses;
var
classByName : TStringList;

// Enregistrer une classe
procedure registerClassByName(classe: TRefMesClasses);
begin
// Ajout de la classe si elle n'est pas déjà référencée
if (classByName.IndexOf(classe.ClassName) = -1) then
begin
classByName.AddObject(classe.ClassName,TObject(classe));
end;
end;

// Obtenir une classe par son nom
function getClassByName(nom: string): TRefMesClasses;
var
i : integer;
begin
// Recherche dans la liste des classByName
i := classByName.IndexOf(nom);
// si pas trouvé, on renvoie nil
if (i = -1) then
begin
result := nil;
end
// si trouvé, on renvoie la classe associée
else
begin
result := TRefMesClasses(classByName.objects[i]);
end
end;

// Au début de l'application, enregistrer les classes  :
classByName := TStringList.Create;
registerClassByName(MaClasse1);
...
// Fin d'application, libérer classByName
classByName.Free;
// Utilisation :
getClassByName(Edit1.Text).Traitement...

Cette approche générique nous permet d'améliorer le premier code source présenté dans le chapitre «  les références de classe ou métaclasses » : Donc à la place de :

...
case rgClasseObjet.ItemIndex of
0 : ControlClass:=TEdit;
1 : ControlClass:=TButton;
2 : ControlClass:=TMemo;
3 : ControlClass:=TControlClass(TObject);
end;
MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);

Instance :=ControlClass.Create(Self );
Instance.Parent:=Panel1;

on écrira :

procedure TForm1.Creer;
Begin
if (rgClasseObjet.ItemIndex <= -1)
then Exit;

FreeAndNil(Instance);

with rgClasseObjet do
// Appel la méthode de classe
Instance := TControlClass(Items.Objects[ItemIndex]).Create(Self);
Instance.Parent:=Panel1;

MemoLog.Lines.Add('Création d''une instance de la classe '+Instance.ClassName);
end;

La méthode FormCreate devenant :

procedure TForm1.FormCreate(Sender: TObject);
begin
ControlClass :=Nil;
Instance :=Nil;
rgClasseObjet.Items.AddObject('TEdit',TObject(TEdit));
rgClasseObjet.Items.AddObject('TButton',TObject(TButton));
rgClasseObjet.Items.AddObject('TMemo',TObject(TMemo));
end;

Code source modifié.