Skip to content

Latest commit

 

History

History
942 lines (764 loc) · 20.9 KB

File metadata and controls

942 lines (764 loc) · 20.9 KB

🔝 Retour au Sommaire

3.7.6 Généricité en Object Pascal

Introduction

La généricité (ou generics en anglais) est un mécanisme qui permet de créer des classes, des méthodes et des types qui peuvent fonctionner avec différents types de données sans avoir à réécrire le code pour chaque type.

Pourquoi utiliser la généricité ?

Le problème sans généricité

Imaginez que vous voulez créer une liste pour stocker des entiers :

type
  TListeEntiers = class
  private
    FItems: array of Integer;
  public
    procedure Ajouter(Item: Integer);
    function Obtenir(Index: Integer): Integer;
  end;

Maintenant vous voulez une liste pour des chaînes de caractères. Il faut créer une autre classe :

type
  TListeChaines = class
  private
    FItems: array of string;
  public
    procedure Ajouter(Item: string);
    function Obtenir(Index: Integer): string;
  end;

Et pour des personnes, encore une autre classe :

type
  TListePersonnes = class
  private
    FItems: array of TPersonne;
  public
    procedure Ajouter(Item: TPersonne);
    function Obtenir(Index: Integer): TPersonne;
  end;

Problème : Beaucoup de duplication de code !

La solution avec généricité

Avec la généricité, vous créez une seule classe qui fonctionne avec n'importe quel type :

type
  TListe<T> = class
  private
    FItems: array of T;
  public
    procedure Ajouter(Item: T);
    function Obtenir(Index: Integer): T;
  end;

Le <T> signifie "type générique". T est un paramètre de type qui sera remplacé par le type réel lors de l'utilisation.

Analogie du monde réel

Pensez à un moule à gâteau en silicone :

  • Le moule peut faire des gâteaux au chocolat, à la vanille, aux fruits...
  • C'est toujours le même moule (la classe générique)
  • Mais le contenu change (le type T)
  • Vous n'avez pas besoin d'un moule différent pour chaque saveur !

Déclaration d'une classe générique

Syntaxe de base

type
  TMaClasse<T> = class
  private
    FValeur: T;
  public
    constructor Create(AValeur: T);
    function ObtenirValeur: T;
    procedure DefinirValeur(AValeur: T);
    property Valeur: T read FValeur write FValeur;
  end;

Implémentation

constructor TMaClasse<T>.Create(AValeur: T);  
begin  
  inherited Create;
  FValeur := AValeur;
end;

function TMaClasse<T>.ObtenirValeur: T;  
begin  
  Result := FValeur;
end;

procedure TMaClasse<T>.DefinirValeur(AValeur: T);  
begin  
  FValeur := AValeur;
end;

Utilisation

var
  ConteneurEntier: TMaClasse<Integer>;
  ConteneurChaine: TMaClasse<string>;
  ConteneurDouble: TMaClasse<Double>;
begin
  // Créer un conteneur d'entiers
  ConteneurEntier := TMaClasse<Integer>.Create(42);
  try
    ShowMessage('Valeur entière : ' + IntToStr(ConteneurEntier.Valeur));
  finally
    ConteneurEntier.Free;
  end;

  // Créer un conteneur de chaînes
  ConteneurChaine := TMaClasse<string>.Create('Bonjour');
  try
    ShowMessage('Valeur chaîne : ' + ConteneurChaine.Valeur);
  finally
    ConteneurChaine.Free;
  end;

  // Créer un conteneur de doubles
  ConteneurDouble := TMaClasse<Double>.Create(3.14);
  try
    ShowMessage('Valeur double : ' + FloatToStr(ConteneurDouble.Valeur));
  finally
    ConteneurDouble.Free;
  end;
end;

Exemple pratique : Pile générique

Une pile (stack) est une structure de données LIFO (Last In, First Out - dernier entré, premier sorti).

type
  TPile<T> = class
  private
    FItems: array of T;
    FCount: Integer;
  public
    constructor Create;
    procedure Empiler(Item: T);
    function Depiler: T;
    function EstVide: Boolean;
    function Sommet: T;
    property Count: Integer read FCount;
  end;

constructor TPile<T>.Create;  
begin  
  inherited Create;
  SetLength(FItems, 0);
  FCount := 0;
end;

procedure TPile<T>.Empiler(Item: T);  
begin  
  Inc(FCount);
  SetLength(FItems, FCount);
  FItems[FCount - 1] := Item;
end;

function TPile<T>.Depiler: T;  
begin  
  if EstVide then
    raise Exception.Create('La pile est vide');

  Result := FItems[FCount - 1];
  Dec(FCount);
  SetLength(FItems, FCount);
end;

function TPile<T>.EstVide: Boolean;  
begin  
  Result := (FCount = 0);
end;

function TPile<T>.Sommet: T;  
begin  
  if EstVide then
    raise Exception.Create('La pile est vide');

  Result := FItems[FCount - 1];
end;

Utilisation de la pile générique

var
  PileEntiers: TPile<Integer>;
  PileChaines: TPile<string>;
begin
  // Pile d'entiers
  PileEntiers := TPile<Integer>.Create;
  try
    PileEntiers.Empiler(10);
    PileEntiers.Empiler(20);
    PileEntiers.Empiler(30);

    ShowMessage('Sommet : ' + IntToStr(PileEntiers.Sommet));  // 30
    ShowMessage('Dépiler : ' + IntToStr(PileEntiers.Depiler));  // 30
    ShowMessage('Nouveau sommet : ' + IntToStr(PileEntiers.Sommet));  // 20
  finally
    PileEntiers.Free;
  end;

  // Pile de chaînes
  PileChaines := TPile<string>.Create;
  try
    PileChaines.Empiler('Premier');
    PileChaines.Empiler('Deuxième');
    PileChaines.Empiler('Troisième');

    while not PileChaines.EstVide do
      ShowMessage(PileChaines.Depiler);
  finally
    PileChaines.Free;
  end;
end;

Plusieurs paramètres de type

Une classe peut avoir plusieurs paramètres de type :

type
  TPaire<TKey, TValue> = class
  private
    FCle: TKey;
    FValeur: TValue;
  public
    constructor Create(ACle: TKey; AValeur: TValue);
    property Cle: TKey read FCle write FCle;
    property Valeur: TValue read FValeur write FValeur;
  end;

constructor TPaire<TKey, TValue>.Create(ACle: TKey; AValeur: TValue);  
begin  
  inherited Create;
  FCle := ACle;
  FValeur := AValeur;
end;

Utilisation

var
  PaireEntierChaine: TPaire<Integer, string>;
  PaireChaineBooleen: TPaire<string, Boolean>;
begin
  // Paire Integer-String
  PaireEntierChaine := TPaire<Integer, string>.Create(1, 'Premier');
  try
    ShowMessage(Format('Clé : %d, Valeur : %s',
                       [PaireEntierChaine.Cle, PaireEntierChaine.Valeur]));
  finally
    PaireEntierChaine.Free;
  end;

  // Paire String-Boolean
  PaireChaineBooleen := TPaire<string, Boolean>.Create('Actif', True);
  try
    if PaireChaineBooleen.Valeur then
      ShowMessage(PaireChaineBooleen.Cle + ' est actif');
  finally
    PaireChaineBooleen.Free;
  end;
end;

Contraintes de type

Parfois, vous voulez restreindre les types qui peuvent être utilisés avec votre classe générique. C'est ce qu'on appelle les contraintes.

Types de contraintes

type
  // T doit être une classe
  TMaClasse1<T: class> = class
  end;

  // T doit être un record
  TMaClasse2<T: record> = class
  end;

  // T doit avoir un constructeur sans paramètres
  TMaClasse3<T: constructor> = class
  end;

  // T doit descendre de TStream
  TMaClasse4<T: TStream> = class
  end;

  // Plusieurs contraintes
  TMaClasse5<T: class, constructor> = class
  end;

Exemple avec contrainte

type
  // TFactory peut créer n'importe quelle classe avec un constructeur
  TFactory<T: class, constructor> = class
  public
    class function Creer: T;
  end;

class function TFactory<T>.Creer: T;  
begin  
  Result := T.Create;  // Possible car T a la contrainte 'constructor'
end;

Utilisation

type
  TPersonne = class
  public
    Nom: string;
    constructor Create;
  end;

constructor TPersonne.Create;  
begin  
  inherited Create;
  Nom := 'Sans nom';
end;

var
  Factory: TFactory<TPersonne>;
  Personne: TPersonne;
begin
  Personne := TFactory<TPersonne>.Creer;
  try
    ShowMessage('Nom : ' + Personne.Nom);
  finally
    Personne.Free;
  end;
end;

Méthodes génériques

Les méthodes peuvent aussi être génériques, indépendamment de leur classe :

type
  TUtilitaires = class
  public
    class function Max<T>(A, B: T): T;
    class function Min<T>(A, B: T): T;
    class procedure Echanger<T>(var A, B: T);
  end;

class function TUtilitaires.Max<T>(A, B: T): T;  
var  
  Comparer: IComparer<T>;
begin
  Comparer := TComparer<T>.Default;
  if Comparer.Compare(A, B) > 0 then
    Result := A
  else
    Result := B;
end;

class function TUtilitaires.Min<T>(A, B: T): T;  
var  
  Comparer: IComparer<T>;
begin
  Comparer := TComparer<T>.Default;
  if Comparer.Compare(A, B) < 0 then
    Result := A
  else
    Result := B;
end;

class procedure TUtilitaires.Echanger<T>(var A, B: T);  
var  
  Temp: T;
begin
  Temp := A;
  A := B;
  B := Temp;
end;

Utilisation des méthodes génériques

var
  X, Y: Integer;
  Nom1, Nom2: string;
  Prix1, Prix2: Double;
begin
  // Avec des entiers
  X := 10;
  Y := 20;
  ShowMessage('Max : ' + IntToStr(TUtilitaires.Max<Integer>(X, Y)));  // 20

  TUtilitaires.Echanger<Integer>(X, Y);
  ShowMessage(Format('Après échange : X=%d, Y=%d', [X, Y]));  // X=20, Y=10

  // Avec des chaînes
  Nom1 := 'Alice';
  Nom2 := 'Bob';
  ShowMessage('Max : ' + TUtilitaires.Max<string>(Nom1, Nom2));  // Bob

  // Avec des doubles
  Prix1 := 19.99;
  Prix2 := 24.99;
  ShowMessage('Min : ' + FloatToStr(TUtilitaires.Min<Double>(Prix1, Prix2)));  // 19.99
end;

Collections génériques de Delphi

Delphi fournit des collections génériques prêtes à l'emploi dans l'unité System.Generics.Collections :

TList - Liste dynamique

uses System.Generics.Collections;

var
  ListeNombres: TList<Integer>;
  ListeNoms: TList<string>;
  Nombre: Integer;
  Nom: string;
begin
  // Liste d'entiers
  ListeNombres := TList<Integer>.Create;
  try
    ListeNombres.Add(10);
    ListeNombres.Add(20);
    ListeNombres.Add(30);

    for Nombre in ListeNombres do
      ShowMessage(IntToStr(Nombre));

    ShowMessage('Premier élément : ' + IntToStr(ListeNombres[0]));
    ShowMessage('Nombre d''éléments : ' + IntToStr(ListeNombres.Count));
  finally
    ListeNombres.Free;
  end;

  // Liste de chaînes
  ListeNoms := TList<string>.Create;
  try
    ListeNoms.Add('Alice');
    ListeNoms.Add('Bob');
    ListeNoms.Add('Charlie');

    for Nom in ListeNoms do
      ShowMessage(Nom);

    ListeNoms.Sort;  // Tri automatique

    if ListeNoms.Contains('Bob') then
      ShowMessage('Bob est dans la liste');
  finally
    ListeNoms.Free;
  end;
end;

TDictionary<TKey, TValue> - Dictionnaire

Un dictionnaire stocke des paires clé-valeur :

uses System.Generics.Collections;

var
  Ages: TDictionary<string, Integer>;
  Paire: TPair<string, Integer>;
begin
  Ages := TDictionary<string, Integer>.Create;
  try
    // Ajouter des éléments
    Ages.Add('Alice', 25);
    Ages.Add('Bob', 30);
    Ages.Add('Charlie', 28);

    // Accéder à une valeur
    ShowMessage('Âge de Bob : ' + IntToStr(Ages['Bob']));

    // Vérifier l'existence d'une clé
    if Ages.ContainsKey('Alice') then
      ShowMessage('Alice est présente');

    // Parcourir le dictionnaire
    for Paire in Ages do
      ShowMessage(Format('%s a %d ans', [Paire.Key, Paire.Value]));

    // Modifier une valeur
    Ages['Bob'] := 31;

    // Supprimer un élément
    Ages.Remove('Charlie');

  finally
    Ages.Free;
  end;
end;

TObjectList - Liste d'objets avec gestion automatique

uses System.Generics.Collections;

type
  TPersonne = class
  public
    Nom: string;
    Age: Integer;
    constructor Create(ANom: string; AAge: Integer);
  end;

constructor TPersonne.Create(ANom: string; AAge: Integer);  
begin  
  inherited Create;
  Nom := ANom;
  Age := AAge;
end;

var
  Personnes: TObjectList<TPersonne>;
  Personne: TPersonne;
begin
  // TObjectList libère automatiquement les objets !
  Personnes := TObjectList<TPersonne>.Create(True);  // True = OwnsObjects
  try
    Personnes.Add(TPersonne.Create('Alice', 25));
    Personnes.Add(TPersonne.Create('Bob', 30));
    Personnes.Add(TPersonne.Create('Charlie', 28));

    for Personne in Personnes do
      ShowMessage(Format('%s a %d ans', [Personne.Nom, Personne.Age]));

    // Les objets seront automatiquement libérés !
  finally
    Personnes.Free;  // Libère la liste ET tous les objets qu'elle contient
  end;
end;

TQueue - File (FIFO)

uses System.Generics.Collections;

var
  MaFile: TQueue<string>;
begin
  MaFile := TQueue<string>.Create;
  try
    // Ajouter des éléments
    MaFile.Enqueue('Premier');
    MaFile.Enqueue('Deuxième');
    MaFile.Enqueue('Troisième');

    // Retirer dans l'ordre FIFO (First In, First Out)
    while MaFile.Count > 0 do
      ShowMessage(MaFile.Dequeue);

    // Affichera : Premier, Deuxième, Troisième
  finally
    MaFile.Free;
  end;
end;

TStack - Pile (LIFO)

uses System.Generics.Collections;

var
  Pile: TStack<Integer>;
begin
  Pile := TStack<Integer>.Create;
  try
    // Empiler des éléments
    Pile.Push(10);
    Pile.Push(20);
    Pile.Push(30);

    // Dépiler dans l'ordre LIFO (Last In, First Out)
    while Pile.Count > 0 do
      ShowMessage(IntToStr(Pile.Pop));

    // Affichera : 30, 20, 10
  finally
    Pile.Free;
  end;
end;

Exemple complet : Gestionnaire de cache générique

uses System.Generics.Collections;

type
  TCache<TKey, TValue> = class
  private
    FDonnees: TDictionary<TKey, TValue>;
    FCapaciteMax: Integer;
  public
    constructor Create(ACapacite: Integer);
    destructor Destroy; override;
    procedure Ajouter(Cle: TKey; Valeur: TValue);
    function Obtenir(Cle: TKey): TValue;
    function Existe(Cle: TKey): Boolean;
    procedure Supprimer(Cle: TKey);
    procedure Vider;
    property Count: Integer read GetCount;
  private
    function GetCount: Integer;
  end;

constructor TCache<TKey, TValue>.Create(ACapacite: Integer);  
begin  
  inherited Create;
  FDonnees := TDictionary<TKey, TValue>.Create;
  FCapaciteMax := ACapacite;
end;

destructor TCache<TKey, TValue>.Destroy;  
begin  
  FDonnees.Free;
  inherited Destroy;
end;

procedure TCache<TKey, TValue>.Ajouter(Cle: TKey; Valeur: TValue);  
begin  
  // Si le cache est plein, supprimer le premier élément
  if FDonnees.Count >= FCapaciteMax then
  begin
    // Simplification : supprimer un élément aléatoire
    // En pratique, on implémenterait LRU (Least Recently Used)
    var PremiereCle := FDonnees.Keys.ToArray[0];
    FDonnees.Remove(PremiereCle);
  end;

  FDonnees.AddOrSetValue(Cle, Valeur);
end;

function TCache<TKey, TValue>.Obtenir(Cle: TKey): TValue;  
begin  
  if not FDonnees.TryGetValue(Cle, Result) then
    raise Exception.Create('Clé non trouvée dans le cache');
end;

function TCache<TKey, TValue>.Existe(Cle: TKey): Boolean;  
begin  
  Result := FDonnees.ContainsKey(Cle);
end;

procedure TCache<TKey, TValue>.Supprimer(Cle: TKey);  
begin  
  FDonnees.Remove(Cle);
end;

procedure TCache<TKey, TValue>.Vider;  
begin  
  FDonnees.Clear;
end;

function TCache<TKey, TValue>.GetCount: Integer;  
begin  
  Result := FDonnees.Count;
end;

Utilisation du cache

var
  CacheUtilisateurs: TCache<Integer, string>;
  CachePrix: TCache<string, Double>;
begin
  // Cache d'utilisateurs (ID -> Nom)
  CacheUtilisateurs := TCache<Integer, string>.Create(100);
  try
    CacheUtilisateurs.Ajouter(1, 'Alice');
    CacheUtilisateurs.Ajouter(2, 'Bob');
    CacheUtilisateurs.Ajouter(3, 'Charlie');

    if CacheUtilisateurs.Existe(2) then
      ShowMessage('Utilisateur 2 : ' + CacheUtilisateurs.Obtenir(2));

    ShowMessage('Éléments en cache : ' + IntToStr(CacheUtilisateurs.Count));
  finally
    CacheUtilisateurs.Free;
  end;

  // Cache de prix (Produit -> Prix)
  CachePrix := TCache<string, Double>.Create(50);
  try
    CachePrix.Ajouter('Pomme', 2.50);
    CachePrix.Ajouter('Orange', 3.00);
    CachePrix.Ajouter('Banane', 1.80);

    ShowMessage('Prix de la pomme : ' + FloatToStr(CachePrix.Obtenir('Pomme')));
  finally
    CachePrix.Free;
  end;
end;

Comparaison et tri avec génériques

Delphi fournit IComparer<T> pour comparer des éléments :

uses System.Generics.Collections, System.Generics.Defaults;

type
  TPersonne = class
  public
    Nom: string;
    Age: Integer;
    constructor Create(ANom: string; AAge: Integer);
  end;

constructor TPersonne.Create(ANom: string; AAge: Integer);  
begin  
  inherited Create;
  Nom := ANom;
  Age := AAge;
end;

var
  Personnes: TList<TPersonne>;
  Personne: TPersonne;
  ComparerParAge: IComparer<TPersonne>;
begin
  Personnes := TList<TPersonne>.Create;
  try
    Personnes.Add(TPersonne.Create('Charlie', 28));
    Personnes.Add(TPersonne.Create('Alice', 25));
    Personnes.Add(TPersonne.Create('Bob', 30));

    // Créer un comparateur personnalisé
    ComparerParAge := TComparer<TPersonne>.Construct(
      function(const A, B: TPersonne): Integer
      begin
        Result := A.Age - B.Age;
      end
    );

    // Trier par âge
    Personnes.Sort(ComparerParAge);

    ShowMessage('Liste triée par âge :');
    for Personne in Personnes do
      ShowMessage(Format('%s (%d ans)', [Personne.Nom, Personne.Age]));

  finally
    for Personne in Personnes do
      Personne.Free;
    Personnes.Free;
  end;
end;

Avantages de la généricité

  1. Réutilisabilité : un seul code pour plusieurs types
  2. Sécurité des types : erreurs détectées à la compilation
  3. Performance : pas de boxing/unboxing comme avec TObject
  4. Lisibilité : le code est plus clair et explicite
  5. Maintenance : moins de duplication de code

Comparaison : avec et sans génériques

// ❌ Sans génériques (ancienne méthode)
var
  Liste: TList;  // TList de TObject
  Personne: TPersonne;
begin
  Liste := TList.Create;
  try
    Liste.Add(TPersonne.Create('Alice', 25));

    // Nécessite un cast !
    Personne := TPersonne(Liste[0]);

    // Risque : si on met le mauvais type, erreur à l'exécution
    Liste.Add(TStringList.Create);  // Oups !
  finally
    Liste.Free;
  end;
end;

// ✅ Avec génériques (méthode moderne)
var
  Liste: TObjectList<TPersonne>;
  Personne: TPersonne;
begin
  Liste := TObjectList<TPersonne>.Create(True);
  try
    Liste.Add(TPersonne.Create('Alice', 25));

    // Pas de cast nécessaire !
    Personne := Liste[0];

    // Erreur de compilation si mauvais type
    // Liste.Add(TStringList.Create);  // Ne compile pas !
  finally
    Liste.Free;
  end;
end;

Bonnes pratiques

1. Utilisez des noms de paramètres explicites

// ✅ Bon - noms explicites
TDictionnaire<TKey, TValue> = class

// ⚠️ Acceptable mais moins clair
TDictionnaire<K, V> = class

// ❌ Éviter - pas assez clair
TDictionnaire<T1, T2> = class

2. Préférez les collections génériques aux anciennes

// ✅ Bon
var Liste: TList<Integer>;

// ❌ Éviter (ancienne méthode)
var Liste: TList;  // Liste de TObject

3. Utilisez TObjectList pour les objets

// ✅ Bon - libération automatique
var Personnes: TObjectList<TPersonne>;  
Personnes := TObjectList<TPersonne>.Create(True);  

// ❌ Plus risqué - libération manuelle
var Personnes: TList<TPersonne>;  
Personnes := TList<TPersonne>.Create;  
// Il faut libérer chaque objet manuellement !

4. Utilisez les contraintes quand nécessaire

// ✅ Bon - contrainte appropriée
TFactory<T: class, constructor> = class

// ⚠️ Pas de contrainte - moins sûr
TFactory<T> = class

5. Documentez vos classes génériques

/// <summary>
/// Cache générique avec capacité limitée
/// </summary>
/// <typeparam name="TKey">Type de la clé</typeparam>
/// <typeparam name="TValue">Type de la valeur</typeparam>
TCache<TKey, TValue> = class

Limitations de la généricité

  1. Pas de spécialisation : on ne peut pas avoir des implémentations différentes pour des types spécifiques
  2. Pas de valeurs par défaut pour T : on ne peut pas faire FValeur: T = DefaultValue
  3. Contraintes limitées : les contraintes sont moins puissantes que dans d'autres langages

Résumé

  • Généricité = mécanisme permettant de créer du code réutilisable pour différents types

    • Syntaxe : TMaClasse<T>T est le paramètre de type
    • Évite la duplication de code
  • Paramètres de type

    • Un seul : TListe<T>
    • Plusieurs : TDictionnaire<TKey, TValue>
    • Noms conventionnels : T, TKey, TValue, etc.
  • Contraintes

    • class : doit être une classe
    • record : doit être un record
    • constructor : doit avoir un constructeur
    • Classe de base : T: TStream
  • Collections génériques Delphi

    • TList<T> : liste dynamique
    • TDictionary<TKey, TValue> : dictionnaire
    • TObjectList<T> : liste d'objets avec gestion automatique
    • TQueue<T> : file FIFO
    • TStack<T> : pile LIFO
  • Avantages

    • Sécurité des types à la compilation
    • Pas de cast nécessaire
    • Meilleure performance
    • Code plus lisible et maintenable

La généricité est un outil puissant et moderne en Delphi. Elle permet d'écrire du code plus sûr, plus performant et plus facile à maintenir. Les collections génériques doivent être privilégiées dans tout nouveau code.

⏭️ Modèles de conception (Design Patterns)