IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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

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

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

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

 
Sélectionnez
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

  • ..\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

 
Sélectionnez
...
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 :

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

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

 
Sélectionnez
{ 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 :

 
Sélectionnez
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.

 
Sélectionnez
...
 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.

 
Sélectionnez
...
  // 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 :

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

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

 
Sélectionnez
OnClick := MainForm.ButtonClick;

Les déclarations suivantes sont possibles :

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

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

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

 
Sélectionnez
class of type

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.

 
Sélectionnez
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.

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

 
Sélectionnez
...
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.

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

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

Rappel

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

 
Sélectionnez
type TMaClasse = class
 ...
end;

est équivalente à :

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

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

 
Sélectionnez
{ 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 :

 
Sélectionnez
Var
 ComponentClass : TComponentClass ;
 ControlClass   : TControlClass

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

Alors que dans ces affectations seules la seconde est valide :

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

 
Sélectionnez
if Instance is TControl
 thenelse
 if Instance is TComponent
   thenelse
    if Instance is TPersistent
     thenelse
      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.

 
Sélectionnez
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.

 
Sélectionnez
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.

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

 
Sélectionnez
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.

Image non disponible

Première version du code

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
procedure TForm1.Creer;
Begin
  FreeAndNil(Instance);
  case rgClasseObjet.ItemIndex of
    0 : ControlClass:=TEdit;
    1 : ControlClass:=TButton;
    2 : ControlClass:=TMemo;
  end;
                                                         // Appel la méthode de classe
  MemoLog.Lines.Add('Création d''une instance de la classe '+ControlClass.ClassName);
  // provoque une erreur lors de la compilation :
  // MemoLog.Lines.Add('Création d''une instance de classe '+ControlClass.ClassType);

  Instance :=ControlClass.Create(Self );
  Instance.Parent:=Panel1;
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 :

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

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

 
Sélectionnez
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

 
Sélectionnez
...
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 :

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

 
Sélectionnez
3 : ControlClass:=TControlClass(TObject);

et dans la méthode Fabriquer :

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

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

 
Sélectionnez
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.

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

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

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

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

 
Sélectionnez
...
  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.

 
Sélectionnez
...
  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.

 
Sélectionnez
// 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 :

 
Sélectionnez
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.

 
Sélectionnez
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

  • ..\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 :

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

 
Sélectionnez
class function ClassName: ShortString;

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

 
Sélectionnez
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.

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

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

 
Sélectionnez
TControlClass=TEdit.Text; // Invalide

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

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

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

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

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

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

Exemple :

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

 
Sélectionnez
...
 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 :

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

 
Sélectionnez
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é.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

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