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

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

II-A. 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 utilisé 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 :

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

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

II-B. 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 :

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

II-C. 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 disparu).

Les nouvelles méthodes sont les suivantes :

  • trois 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 ;
  • deux versions surchargées de Sort (le nom n'est pas nouveau, mais l'utilisation est tout autre) ;
  • deux 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 toute façon é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.

II-D. 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).

II-D-1. É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 :

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

 
Sélectionnez
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 DelphiLes interfaces d'objet sous Delphi, par Laurent Dardenne de Laurent Dardenne.

II-D-2. É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 :

 
Sélectionnez
type
  TNotifyEvent = procedure(Sender: TObject) of object;

Les types référence de routine sont déclarés, dans cet exemple, comme TComparison<T> :

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

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

Mais j'ai lâché le morceau plus haut, on peut assigner une routine anonyme à une référence de routine. Alors, voyons ce que cela pourrait donner :

 
Sélectionnez
procedure SortPointsWithAnonymous;
var
  List: TList<TPoint>;
  // ...
begin
  List := TList<TPoint>.Create(TComparer<TPoint>.Construct(
    function(const Left, Right: TPoint): Integer
    begin
      Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
    end));

  // Toujours la même suite...
end;

Cette forme de création est intéressante surtout si c'est le seul endroit où vous aurez besoin de la routine de comparaison.

En parlant des routines anonymes : oui, elles peuvent accéder à des variables locales de la routine/méthode englobante. Eh oui, elles peuvent encore le faire après le retour de cette routine/méthode englobante. L'exemple suivant montre la chose :

 
Sélectionnez
function MakeComparer(Reverse: Boolean = False): TComparison<TPoint>;
begin
  Result :=
    function(const Left, Right: TPoint): Integer
    begin
      Result := DistanceToCenterSquare(Left) - DistanceToCenterSquare(Right);
      if Reverse then
        Result := -Result;
    end;
end;

procedure SortPointsWithAnonymous;
var
  List: TList<TPoint>;
  // ...
begin
  List := TList<TPoint>.Create(TComparer<TPoint>.Construct(
    MakeComparer(True)));

  // Toujours la même suite...
end;

Intéressant, n'est-ce pas ?

Voilà qui clôt ce petit tour des comparateurs utilisés avec TList<T>, et avec lui cette introduction aux génériques à travers l'utilisation de cette classe. Dans le chapitre suivant, nous allons commencer à voir comment on peut écrire soi-même une classe générique.


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.