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

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 descendantes ; 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 contraintes 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 doive 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 nœud 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 contrainte 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 points-virgules (;), é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 contraintes 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 points-virgules 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 records 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

Copyright © 2008 Sébastien Doeraene. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.