Les génériques avec Delphi 2009 Win32

Avec en bonus les routines anonymes et les références de routine


précédentsommairesuivant

V. Contraintes sur les types génériques

Bien, vous connaissez maintenant les bases. Il est temps de passer aux choses sérieuses, à savoir : les contraintes.

Comme leur nom l'indique, les contraintes permettent d'imposer des restrictions sur les types réels qui peuvent être utilisés pour remplacer un paramètre formel de type. Pour continuer la comparaison avec les paramètres de méthode : une contrainte est au type ce que le type est à la variable paramètre. Pas clair ? Bon, lorsque vous spécifiez un type pour un paramètre, vous ne pouvez lui transmettre que des valeurs qui sont compatibles avec ce type. Lorsque vous spécifiez une contrainte sur un paramètre de type, vous devez le remplacer par un type réel qui satisfait ces contraintes.

V-A. Quelles sont les contraintes possibles ?

Il n'existe qu'un nombre restreint de contraintes possibles. En fait, il n'en existe que de trois types, dont un pour lequel je n'ai pas trouvé d'utilité :-s.

On peut donc restreindre un type générique à être :

  • un type classe descendant d'une classe donnée ;
  • un type interface descendant d'une interface donnée, ou un type classe implémentant cette interface ;
  • un type ordinal, flottant ou record ;
  • un type classe qui possède un constructeur sans argument.

Pour imposer une contrainte à un paramètre générique, on note :

 
Sélectionnez

type
  TStreamGenericType<T: TStream> = class
  end;

  TIntfListGenericType<T: IInterfaceList> = class
  end;

  TSimpleTypeGenericType<T: record> = class
  end;

  TConstructorGenericType<T: constructor> = class
  end;
				

Ce qui impose, respectivement, que T devra être remplacé par la classe TStream ou une de ses descendantes ; par IInterfaceList ou une de ses descendante ; par un type ordinal, flottant ou record (un type de valeur non null, selon la terminologie de Delphi) ; ou enfin par un type classe qui possède un constructeur public sans argument.

<T: class> doit être employé à la place de <T: TObject>... On comprend bien que class soit accepté mais on se demande pourquoi TObject est rejeté.

Il est possible de combiner plusieurs contraintes interface, ou une contrainte classe et une à plusieurs contraintes interface. Dans ce cas, le type réel utilisé doit satisfaire toutes les contraintes en même temps. La contrainte constructor peut également être combinée avec des constraintes classe et/ou interface. Il est même possible de combiner record avec une ou plusieurs contraintes interface, mais je ne vois pas comment un type pourrait bien satisfaire les deux en même temps (en .NET, c'est possible, mais, pour l'instant, pas en Win32) !

Il se pourrait, mais ce n'est que pure spéculation de ma part, que ce soit en prévision d'une version future, dans laquelle le type Integer, par exemple, "implémenterait" l'interface IComparable<Integer>. Ce qui en ferait donc un type satisfaisant les deux contraintes d'une déclaration comme <T: record, IComparable<T>>.

On peut aussi utiliser une classe ou interface générique comme contrainte, avec donc un paramètre à spécifier. Ce paramètre peut être le type T lui-même. Par exemple, on peut vouloir imposer que le type d'éléments doit pouvoir se comparer avec lui-même. On utilisera alors :

 
Sélectionnez

type
  TSortedList<T: IComparable<T>> = class(TObject)
  end;
				

Si l'on veut en plus que T soit un type classe, on peut combiner :

 
Sélectionnez

type
  TSortedList<T: class, IComparable<T>> = class(TObject)
  end;
				

V-B. Mais à quoi ça sert ?

« Je croyais que le but des génériques était justement d'écrire une fois le code pour tous les types. Quel est alors l'intérêt de restreindre les types possibles ? »

Eh bien cela permet au compilateur d'avoir plus d'informations sur le type utilisé. Par exemple, cela lui permet de savoir, avec un type <T: class>, qu'il est légitime d'appeler la méthode Free dessus. Ou avec un type <T: IComparable<T>>, qu'il est possible d'écrire Left.CompareTo(Right).

Pour illustrer cela, nous allons créer une classe fille de TTreeNode<T>, TObjectTreeNode<T: class>. À l'instar de TObjectList<T: class> qui propose de libérer automatiquement ses éléments lorsque la liste est détruite, notre classe libérera sa valeur étiquetée lors de la destruction.

En fait, cela fait donc très peu de code, que je vais donner en une fois :

 
Sélectionnez

type
  {*
    Structure arborescente générique dont les valeurs sont des objets
    Lorsque le noeud est libéré, la valeur étiquetée est libérée également.
  *}
  TObjectTreeNode<T: class> = class(TTreeNode<T>)
  public
    destructor Destroy; override;
  end;

{--------------------}
{ TObjectTreeNode<T> }
{--------------------}

{*
  [@inheritDoc]
*}
destructor TObjectTreeNode<T>.Destroy;
begin
  Value.Free;
  inherited;
end;
				

Voilà, c'est tout. Le but est uniquement de montrer la technique. Pas d'avoir un truc exceptionnel.

Il y a deux choses à constater ici. D'abord, on peut faire hériter une classe générique d'une autre classe générique, en réutilisant le paramètre générique (ou pas, d'ailleurs).

Ensuite, dans l'implémentation des méthodes d'une classe générique avec contraintes, les contraintes ne doivent pas (et ne peuvent pas) être répétées.

Vous pouvez par ailleurs supprimer la contrainte et tenter de compiler. Le compilateur vous arrêtera sur l'appel à Free. En effet, Free n'est pas disponible sur n'importe quel type. Mais sur n'importe quelle classe, bien.

V-C. Une variante avec constructor

Vous pourriez aussi vouloir que les deux constructeurs sans paramètre AValue de TObjectTreeNode<T> créent un objet pour AValue au lieu d'utiliser Default(T) (qui, au passage, renvoie nil ici car T est contraint à être une classe).

Vous pouvez, pour cela, utiliser la contrainte constructor, ce qui donne :

 
Sélectionnez

type
  {*
    Structure arborescente générique dont les valeurs sont des objets
    Lorsqu'un noeud est créé sans valeur étiquetée, une nouvelle valeur est
    créée avec le constructeur sans paramètre du type choisi.
    Lorsque le noeud est libéré, la valeur étiquetée est libérée également.
  *}
  TCreateObjectTreeNode<T: class, constructor> = class(TObjectTreeNode<T>)
  public
    constructor Create(AParent: TTreeNode<T>); overload;
    constructor Create; overload;
  end;

implementation

{*
  [@inheritDoc]
*}
constructor TCreateObjectTreeNode<T>.Create(AParent: TTreeNode<T>);
begin
  Create(AParent, T.Create);
end;

{*
  [@inheritDoc]
*}
constructor TCreateObjectTreeNode<T>.Create;
begin
  Create(T.Create);
end;
				

À nouveau, si vous retirez la constrainte constructor ici, le compilateur marquera une erreur sur le T.Create.

VI. Paramétriser une classe avec plusieurs types

Comme vous avez pu vous en douter, il est possible de paramétriser une classe avec plusieurs types. Chacun, éventuellement, avec ses contraintes.

Ainsi, la classe TDictionary<TKey,TValue> prend en paramètres deux types. Le premier est le type des clefs, le second le type des éléments. Cette classe implémente une table de hachage.

Ne vous y trompez pas : TKey et TValue sont bien des paramètres génériques (formels), pas des types réels. Ne vous laissez pas prendre au piège par la notation.

La syntaxe de déclaration est un peu laxiste, sur ce point. Il est en effet possible de séparer les types par des virgules (,) ou par des point-virgule (;), éventuellement en mixant les deux quand il y a plus de deux types. Autant au niveau de la déclaration de la classe qu'au niveau de l'implémentation des méthodes. Par contre, au niveau de l'utilisation d'un type générique, vous devez utiliser des virgules !

Toutefois, si vous placez une ou plusieurs contrainte sur un type qui n'est pas le dernier de la liste, vous devrez utiliser un point-virgule pour le séparer du suivant. En effet, une virgule signalerait une seconde contrainte.

Aussi, permettez-moi de vous proposer une règle de style - qui n'est pas celle suivie par Embarcadero. Utilisez toujours des point-virgule dans la déclaration du type générique (là où vous êtes susceptible de pouvoir mettre des contraintes) et utilisez des virgules partout ailleurs (implémentation des méthodes, et utilisation du type).

Comme je n'ai pas de meilleur exemple de type générique à vous proposer que celle de TDictionary<TKey,TValue>, je vous conseille de consulter le code de cette classe (définie dans l'unité Generics.Collections, vous vous en doutiez probablement). En voici juste un extrait :

 
Sélectionnez

type
  TPair<TKey,TValue> = record
    Key: TKey;
    Value: TValue;
  end;

  // Hash table using linear probing
  TDictionary<TKey,TValue> = class(TEnumerable<TPair<TKey,TValue>>)
    // ...
  public
    // ...

    procedure Add(const Key: TKey; const Value: TValue);
    procedure Remove(const Key: TKey);
    procedure Clear;

    property Items[const Key: TKey]: TValue read GetItem write SetItem; default;
    property Count: Integer read FCount;

    // ...
  end;
			

Comme vous l'avez déjà remarqué, TDictionary<TKey,TValue> utilise des noms de types génériques plus explicites que les T que nous avons utilisé jusqu'ici. Vous devriez faire de même, à chaque fois que le type a une signification particulière, comme c'est le cas ici. Et d'autant plus lorsqu'il y a plus d'un type paramétré.

VII. Autres types génériques

Jusqu'à présent, nous n'avons défini que des classes génériques. Pourtant, nous avons déjà rencontré des interfaces génériques (comme IComparer<T>) et vous venez de croiser un type record générique (TPair<TKey,TValue>).

Il est donc tout à fait possible de définir autant des interfaces ou des record génériques que des classes génériques. Il est également possible de déclarer un type tableau générique (statique ou dynamique) mais dont seul le type des éléments peut dépendre des types paramétrés ; mais il est peu probable que vous y trouviez une utilité réelle.

Il n'est donc pas possible de déclarer un type pointeur générique, ou un type ensemble générique :

 
Sélectionnez

type
  TGenericPointer<T> = ^T; // erreur de compilation
  TGenericSet<T> = set of T; // erreur de compilation
			

précédentsommairesuivant

Tutoriels
Les génériques avec Delphi 2009 Win32 (English version) - également disponible en espagnol et en russe
Réaliser un plug-in comportant un composant
Construire une procédure pointant sur une méthode
Création de composants - en 4 parties
Refactoring avec Delphi 2007
Prise en main de Delphi 2005
Analyseurs syntaxiques - Leur fonctionnement par l'exemple
Créer un fichier d'aide HLP
Pourquoi un paramètre const change-t-il mystérieusement de valeur ?
Sources
SJRDUnits - Routines et classes diverses
SJRDComps - Quelques composants
Projet Sepi
Présentation
FAQ Sepi
Programmes
FunLabyrinthe - Jeu de labyrinthe très spécial et très fun
TrickTakingGame - Jeux de cartes à plis en ligne
MultiAgenda - Agenda multi-répertoires
DecodeFormulaires - Décode les formulaires
Excel --> HTML - Convertisseur de tableaux Excel en HTML
AddressLinks - Lie les adresses Internet et e-mail d'un document HTML
Vipion - Tic Tac Toe sur 4x4 cases avec jeu de l'ordinateur
BigCalc - Calculatrice de haut niveau
Espace paroissial Astérion de Watermael-Boitsfort
  

Copyright © 2008 Sébastien Doeraene. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.