Cours Delphi paramétriser une classe avec plusieurs types

Cours Delphi paramétriser une classe avec plusieurs types, tutoriel & guide de travaux pratiques Delphi en pdf.

L’utilisation au quotidien : l’exemple TList<T>

Paradoxalement, nous allons commencer par voir comment utiliser une classe générique – c’est un abus de langage, il faudrait dire : modèle de classe générique – au quotidien, et non pas comment en écrire une. Il y a plusieurs bonnes raisons à cela.
D’une part, parce qu’il est beaucoup plus facile de concevoir et d’écrire une classe une fois que l’on a une idée assez précise de la façon dont on va l’utiliser. Et c’est encore plus vrai lorsqu’on découvre un nouveau paradigme de programmation.
D’autre part, la majorité des présentations de l’orienté objet en général explique d’abord comment se servir de classes existantes aussi.

Un code simple de base

Pour commencer en douceur, nous allons écrire un petit programme qui liste les carrés des nombres entiers de 0 à X, X étant défini par l’utilisateur.
Classiquement, on aurait utiliser un tableau dynamique (et ce serait bien mieux, rappelez-vous qu’on traite ici un cas d’école), mais on va utiliser une liste d’entiers.
Voici directement le code :
program TutoGeneriques;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes, Generics.Collections;
procedure WriteSquares(Max: Integer);
var
List: TList<Integer>;
I: Integer;
begin
List := TList<Integer>.Create;
try
for I := 0 to Max do
List.Add(I*I);
for I := 0 to List.Count-1 do
WriteLn(Format(‘%d*%0:d = %d’, [I, List[I]]));
finally
List.Free;
end;
end;
var
Max: Integer;
begin
try
WriteLn(‘Entrez un nombre naturel :’);
ReadLn(Max);
WriteSquares(Max);
except
on E:Exception do
Writeln(E.Classname, ‘: ‘, E.Message);
end;
ReadLn;
end.

Qu’est-ce qui est remarquable dans ce code ?

Tout d’abord, bien sûr, la déclaration de la variable List ainsi que la création de l’instance. Au nom du type TList, on a accolé le paramètre réel (ou assimilé : c’est un type, pas une valeur) entre chevrons < et >. (En anglais, on les appelle angle brackets, ce qui signifie littéralement : parenthèses en forme d’angles.)
Vous pouvez donc voir que, pour utiliser une classe générique, il est nécessaire, à chaque fois que vous écrivez son nom, de lui indiquer un type en paramètre. Pour l’instant, nous utiliserons toujours un type réel à cet endroit.
Certains langages avec génériques permettent ce qu’on appelle l’inférence de type, qui consiste, pour le compilateur,
à deviner un type (ici on parle du paramètre de type). Ce n’est pas le cas de Delphi Win32. C’est pour ça que vous ne pouvez pas écrire :
var
List: TList<Integer>;
begin
List := TList.Create; // manque <Integer> ici

end;
La seconde chose est plus importante, et pourtant moins visible, puisqu’il s’agit d’une absence. En effet, aucun besoin de transtypage d’entier en pointeur et vice versa ! Pratique, non ? Et surtout, tellement plus lisible.
D’autre part, la sécurité de type est mieux gérée. Avec des transtypages, vous courrez toujours le risque de vous tromper, et d’ajouter un pointeur à une liste censée contenir des entiers, ou l’inverse. Si vous utilisez correctement les génériques, vous n’aurez probablement presque plus jamais besoin de transtypage, et donc beaucoup moins d’occasion de faire des erreurs. De plus, si vous tentez d’ajouter un Pointer à un objet de classe TList<Integer>, c’est le compilateur qui vous enverra balader. Avant les génériques, une erreur de ce style pourrait n’avoir été révélée que par une valeur aberrante des mois après la sortie en production !
Notez que j’ai pris le type Integer pour limiter le code qui ne soit pas directement lié à la notion de généricité, mais on aurait pu utiliser n’importe quel type à la place.

Assignations entre classes génériques

Comme vous le savez, quand on parle d’héritage, on peut assigner une instance d’une classe fille à une variable d’une classe mère, mais pas l’inverse. Qu’en est-il avec la généricité ?
Partez d’abord du principe que vous ne pouvez rien faire ! Tous ces exemples sont faux, et ne compilent pas :
var
NormalList: TList;
IntList: TList<Integer>;
ObjectList: TList<TObject>;
ComponentList: TList<TComponent>;
begin
ObjectList := IntList;
ObjectList := NormalList;
NormalList := ObjectList;
ComponentList := ObjectList;
ObjectList := ComponentList; // oui, même ça c’est faux end;
Comme le commentaire le fait remarquer, ce n’est pas parce qu’un TComponent peut être assigné à un TObject qu’un TList<TComponent> peut être assigné à un TList<TObject>. Pour comprendre pourquoi, pensez que TList<TObject>.Add(Value: TObject) permettrait, si l’affectation était valide, d’insérer une valeur de type TObject dans une liste de TComponent !
L’autre remarque importante consiste à préciser que TList<T> n’est en aucun cas une spécialisation de TList, ni une généralisation de celle-ci. En fait, ce sont deux types totalement différents, le premier déclaré dans l’unité Generics.Collections et le second dans Classes !
Nous verrons plus loin dans ce tutoriel, qu’il est possible de faire certaines affectations grâce aux contraintes sur les paramètres génériques.

Les méthodes de TList<T>

Il est intéressant de constater que TList<T> ne propose pas le même ensemble de méthodes que TList. On trouve plus de méthodes de traitement « de plus haut niveau », et moins de méthodes « de bas niveau » (comme Last, qui a disparue).
Les nouvelles méthodes sont les suivantes :
• 3 versions surchargées de AddRange, InsertRange et DeleteRange : elles sont l’équivalent de Add/Insert/ Delete respectivement pour une série d’éléments ;
• Une méthode Contains ;
• Une méthode LastIndexOf ;
• Une méthode Reverse ;
• 2 versions surchargées de Sort (le nom n’est pas nouveau, mais l’utilisation est toute autre) ;
• 2 versions surchargées de BinarySearch.
Si j’attire votre attention sur ces changements qui peuvent vous sembler anodins, c’est parce qu’ils sont très caractéristiques du changement dans la façon de concevoir qu’apportent les génériques.
Quel intérêt, en effet, y aurait-il eu à implémenter une méthode AddRange dans la désormais obsolète TList ? Aucun, car il aurait fallu transtyper chaque élément, tour à tour, donc il aurait fallu de toutes façons écrire une boucle pour construire le tableau à insérer. Alors autant appeler directement Add dans chaque tour de boucle.
Tandis qu’avec la généricité, il suffit d’écrire une fois le code, et il est vraiment valable, pleinement, pour tous les types.
Ce qu’il est important de remarquer et de comprendre ici, c’est que la généricité permet de factoriser beaucoup plus de comportements dans l’écriture d’une seule classe.

TList<T> et les comparateurs

Certes, TList<T> peut travailler avec n’importe quel type. Mais comment peut-elle savoir comment comparer deux éléments ? Comment savoir s’ils sont égaux, afin d’en rechercher un avec IndexOf ? Ou comment savoir si l’un est plus petit que l’autre, afin de trier la liste ?
La réponse vient des comparateurs. Un comparateur est une interface d’objet de type IComparer<T>. Eh oui, on reste en pleine généricité. Ce type est défini dans Generics.Defaults.pas
Lorsque vous créez une TList<T>, vous pouvez passer en paramètre au constructeur un comparateur, qui sera dès lors utilisé pour toutes les méthodes qui en ont besoin. Si vous ne le faites pas, un comparateur par défaut sera utilisé.
Le comparateur par défaut utilisé dépend du type des éléments, bien entendu. Pour l’obtenir, TList<T> appelle la méthode de classe TComparer<T>.Default. Cette méthode fait un travail un peu barbare à base de RTTI pour obtenir la meilleure solution dont il soit capable. Mais elle n’est pas toujours adaptée.
Vous pouvez conserver le comparateur par défaut pour les types de données suivants :
• Les types ordinaux (entiers, caractères, booléens, énumérations) ;
• Les types flottants ;
• Les types ensemble (uniquement pour l’égalité) ;
• Les types chaîne longue Unicode (string, UnicodeString et WideString) ;
• Les types chaîne longue ANSI (AnsiString) mais sans code de page pour les < et > ;
• Les types Variant (uniquement pour l’égalité) ;
• Les types classe (uniquement pour l’égalité – utilise la méthode TObject.Equals) ;
• Les types pointeur, meta-classe et interface (uniquement pour l’égalité) ;
• Les types tableau statique ou dynamique d’ordinaux, de flottants ou d’ensembles (uniquement pour l’égalité).
Pour tous les autres types, le comparateur par défaut fait une comparaison bête et méchante du contenu mémoire de la variable. Il vaut donc mieux alors écrire un comparateur personnalisé.
Pour ce faire, il existe deux moyens simples. L’un est basé sur l’écriture d’une fonction, l’autre sur la dérivation de la classe TComparer<T>. Nous allons les illustrer toutes les deux pour la comparaison de TPoint. Nous considérerons que ce qui classe les points est leur distance au centre – au point (0, 0) – afin d’avoir un ordre total (au sens mathématique du terme).

Écrire un comparateur en dérivant TComparer<T>

Rien de plus simple, vous avez toujours fait ça ! Une seule méthode à surcharger : Compare. Elle doit renvoyer 0 en cas d’égalité, un nombre strictement positif si le paramètre de gauche est supérieur à celui de droite, et un nombre strictement négatif dans le cas contraire.
Voici ce que ça donne :
function DistanceToCenterSquare(const Point: TPoint): Integer; inline; begin
Result := Point.X*Point.X + Point.Y*Point.Y;
end;
type
TPointComparer = class(TComparer<TPoint>)
function Compare(const Left, Right: TPoint): Integer; override; end;
function TPointComparer.Compare(const Left, Right: TPoint): Integer; begin
Result := DistanceToCenterSquare(Left) – DistanceToCenterSquare(Right); end;
Remarquez au passage l’héritage de TPointComparer : elle hérite de TComparer<TPoint>. Vous voyez donc qu’il est possible de faire hériter une classe « simple » d’une classe générique, pour peu qu’on lui fournisse un paramètre réel pour son paramètre générique.
Pour utiliser notre comparateur, il suffit d’en créer une instance et de la passer au constructeur de la liste. Voici un petit programme qui crée 10 points au hasard, les trie et affiche la liste triée.
function DistanceToCenter(const Point: TPoint): Extended; inline; begin
Result := Sqrt(DistanceToCenterSquare(Point));
end;
procedure SortPointsWithTPointComparer;
const
MaxX = 100;
MaxY = 100;
PointCount = 10;
var
List: TList<TPoint>;
I: Integer; Item: TPoint;
begin
List := TList<TPoint>.Create(TPointComparer.Create);
try
for I := 0 to PointCount-1 do
List.Add(Point(Random(2*MaxX+1) – MaxX, Random(2*MaxY+1) – MaxY));
List.Sort; // utilise le comparateur passé au constructeur
for Item in List do
WriteLn(Format(‘%d’#9’%d’#9′(distance au centre = %.2f)’,
[Item.X, Item.Y, DistanceToCenter(Item)]));
finally
List.Free;
end;
end;
begin
try
Randomize;
SortPointsWithTPointComparer;
except
on E:Exception do
Writeln(E.Classname, ‘: ‘, E.Message);
end;
ReadLn;
end.
Et où est la libération du comparateur instancié, dans tout ça ? Tout simplement dans le fait que TList<T> prend comme comparateur une interface de type IComparer<T>. Donc le comptage de références est appliqué (implémenté dans TComparer<T>). Ainsi, il n’est aucun besoin de se soucier de la vie, et donc de la libération, du comparateur.
Si vous n’avez aucune idée du fonctionnement des interfaces sous Delphi, il vous sera
très profitable de consulter le tutoriel Les interfaces d’objet sous Delphi de Laurent Dardenne.

Écrire un comparateur avec une simple fonction de comparaison

Cette alternative semble plus simple, d’après son nom : pas besoin de jouer avec des classes en plus. Pourtant, je la traite en second, car elle introduit un nouveau type de données disponible dans Delphi 2009. Il s’agit des références de routine.
Ah bon ? Vous connaissez ? Non, vous ne connaissez pas 😉 Ce que vous connaissez déjà, ce sont les types procéduraux, déclarés par exemple comme TNotifyEvent :
type
TNotifyEvent = procedure(Sender: TObject) of object;
Les types référence de routine sont déclarés, dans cet exemple, comme TComparison<T> :
type TComparison<T> = reference to function(const Left, Right: T): Integer;
Il y a au moins trois différences entre les types procéduraux et les types référence de routine.
La première est qu’un type référence de routine ne peut pas être marqué comme of object. Autrement dit, on ne peut jamais assigner une méthode à une référence de routine, seulement… Des routines. (Ou du moins, je n’ai pas encore réussi à le faire ^^.)
La deuxième est plus fondamentale : tandis qu’un type procédural (non of object) est un pointeur sur l’adresse de base d’une routine (son point d’entrée), un type référence de routine est en réalité une interface ! Avec comptage de références et ce genre de choses. Toutefois, vous n’aurez vraisemblablement jamais à vous en soucier, car son utilisation au quotidien est identique à celle d’un type procédural.
La dernière est ce qui explique l’apparition des références de routine. On peut assigner une routine anonyme – nous allons voir tout de suite à quoi ça ressemble – à une référence de routine, mais pas à un type procédural. Essayez, vous verrez que ça ne passe pas la compilation. Accessoirement, c’est aussi ce qui explique que les références de routine soient implémentées par des interfaces, mais la réflexion à ce sujet est hors du cadre de ce tutoriel.
Revenons à notre tri de points. Pour créer un comparateur sur base d’une fonction, on utilise une autre méthode de classe de TComparer<T> ; il s’agit de Construct. Cette méthode de classe prend en paramètre une référence de routine de type TComparison<T>. Comme déjà signalé, l’usage des références de routine est très similaire à celui des types procéduraux : on peut employer le nom de la routine comme paramètre, directement. Voici ce que ça donne :
function ComparePoints(const Left, Right: TPoint): Integer; begin
Result := DistanceToCenterSquare(Left) – DistanceToCenterSquare(Right); end;
procedure SortPointsWithComparePoints;
const
MaxX = 100;
MaxY = 100;
PointCount = 10;
var
List: TList<TPoint>;
I: Integer; Item: TPoint;
begin
List := TList<TPoint>.Create(TComparer<TPoint>.Construct(ComparePoints)); try
for I := 0 to PointCount-1 do
List.Add(Point(Random(2*MaxX+1) – MaxX, Random(2*MaxY+1) – MaxY));
List.Sort; // utilise le comparateur passé au constructeur
for Item in List do
WriteLn(Format(‘%d’#9’%d’#9′(distance au centre = %.2f)’,
[Item.X, Item.Y, DistanceToCenter(Item)]));
finally
List.Free;
end;
end;
begin
try
Randomize;
SortPointsWithComparePoints;
except
on E:Exception do
Writeln(E.Classname, ‘: ‘, E.Message);
end;
ReadLn;
end.
La seule différence provient donc, bien sûr, de la création du comparateur. Tout le reste de l’utilisation de la liste est identique (encore heureux !).
En interne, la méthode de classe Construct crée une instance de TDelegatedComparer<T>, qui prend en paramètre de son constructeur la référence de routine qui s’occupera de la comparaison. L’appel à Construct renvoie donc un objet de ce type, sous couvert de l’interface IComparer<T>.
Bon, c’était finalement très simple également. En fait, il faut bien s’en rendre compte : les génériques sont là pour nous faciliter la vie !

I – Introduction
I-A – Pré-requis
I-B – Que sont les génériques ?
II – L’utilisation au quotidien : l’exemple TList<T>
II-A – Un code simple de base
II-B – Assignations entre classes génériques
II-C – Les méthodes de TList<T>
II-D – TList<T> et les comparateurs
II-D-1 – Écrire un comparateur en dérivant TComparer<T>
II-D-2 – Écrire un comparateur avec une simple fonction de comparaison
III – Conception d’une classe générique
III-A – Déclaration de la classe
III-B – Implémentation d’une méthode
III-C – La pseudo-routine Default
III-D – Les références de routine avec les parcours d’arbre
III-D-1 – D’autres méthodes qui utilisent les routines anonymes ?
III-E – Et le reste
III-F – Comment utiliser TTreeNode<T> ?
IV – Conception d’un record générique
V – Contraintes sur les types génériques
V-A – Quelles sont les contraintes possibles ?
V-B – Mais à quoi ça sert ?
V-C – Une variante avec constructor
VI – Paramétriser une classe avec plusieurs types
VII – Autres types génériques
VIII – Méthodes génériques
VIII-A – Une fonction Min générique
VIII-B – La surcharge et les contraintes
VIII-C – Des ajouts pour TList<T>
IX – RTTI et génériques
IX-A – Les changements sur la pseudo-routine TypeInfo
IX-A-1 – Une fonction TypeInfo plus générale
IX-B – Les types génériques ont-ils des RTTI ?
X – Conclusion
XI – Télécharger les sources
XII – Remerciements

………

Cours gratuitTélécharger le cours complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *