🔝 Retour au Sommaire
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.
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 !
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.
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 !
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;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;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;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;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;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;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;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.
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;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;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;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;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;Delphi fournit des collections génériques prêtes à l'emploi dans l'unité System.Generics.Collections :
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;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;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;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;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;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;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;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;- Réutilisabilité : un seul code pour plusieurs types
- Sécurité des types : erreurs détectées à la compilation
- Performance : pas de boxing/unboxing comme avec TObject
- Lisibilité : le code est plus clair et explicite
- Maintenance : moins de duplication de code
// ❌ 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;// ✅ Bon - noms explicites
TDictionnaire<TKey, TValue> = class
// ⚠️ Acceptable mais moins clair
TDictionnaire<K, V> = class
// ❌ Éviter - pas assez clair
TDictionnaire<T1, T2> = class// ✅ Bon
var Liste: TList<Integer>;
// ❌ Éviter (ancienne méthode)
var Liste: TList; // Liste de TObject// ✅ 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 !// ✅ Bon - contrainte appropriée
TFactory<T: class, constructor> = class
// ⚠️ Pas de contrainte - moins sûr
TFactory<T> = class/// <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- Pas de spécialisation : on ne peut pas avoir des implémentations différentes pour des types spécifiques
- Pas de valeurs par défaut pour T : on ne peut pas faire
FValeur: T = DefaultValue - Contraintes limitées : les contraintes sont moins puissantes que dans d'autres langages
-
Généricité = mécanisme permettant de créer du code réutilisable pour différents types
- Syntaxe :
TMaClasse<T>oùTest le paramètre de type - Évite la duplication de code
- Syntaxe :
-
Paramètres de type
- Un seul :
TListe<T> - Plusieurs :
TDictionnaire<TKey, TValue> - Noms conventionnels :
T,TKey,TValue, etc.
- Un seul :
-
Contraintes
class: doit être une classerecord: doit être un recordconstructor: doit avoir un constructeur- Classe de base :
T: TStream
-
Collections génériques Delphi
TList<T>: liste dynamiqueTDictionary<TKey, TValue>: dictionnaireTObjectList<T>: liste d'objets avec gestion automatiqueTQueue<T>: file FIFOTStack<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.