VIII. Méthodes génériques▲
Nous avons exploré jusqu'ici les différentes possibilités offertes par les génériques sur les types définis par le développeur. Mais il est également possible d'écrire des méthodes génériques.
Dans beaucoup de présentations des génériques ou des templates pour d'autres langages, cette forme d'utilisation des génériques est présentée en premier. Mais encore une fois, j'ai préféré vous présenter d'abord ce qui sert souvent avant de m'intéresser aux utilisations moins fréquentes des génériques.
VIII-A. Une fonction Min générique▲
Pour présenter le concept, nous allons écrire une méthode de classe TArrayUtils.Min<T>, qui trouve et renvoie le plus petit élément d'un tableau. Nous aurons donc besoin d'utiliser un comparateur de type IComparer<T>.
Tout comme le nom du type devait l'être, le nom de la méthode doit être suivi des paramètres génériques entre chevrons. Ici le type générique est le type des éléments du tableau.
type
TArrayUtils = class
public
class
function
Min<T>(const
Items: array
of
T;
const
Comparer: IComparer<T>): T; static
;
end
;
Et non ! Il n'est pas possible de déclarer une routine globale avec des paramètres génériques. Une raison possible viendrait du parallélisme avec la syntaxe de Delphi.NET, afin de réduire les coûts de développement et de maintenance, en interne.
Pour pallier ce manque, on utilise donc des méthodes de classe. Et mieux, on la précise comme étant static. Pas grand-chose à voir avec le mot-clef du même nom en C++. Il s'agit ici de liaison statique. C'est-à-dire que dans une telle méthode, il n'y a pas de Self, et que donc l'appel à des méthodes de classe virtuelles, ou à des constructeurs virtuels, n'est pas « virtualisé ». Autrement dit, c'est comme s'il n'était pas virtuel.
Finalement, cela fait de la méthode de classe statique une authentique routine globale, mais avec un espace de noms différent.
Contrairement à certains langages, il n'est pas nécessaire qu'un paramètre au moins reprenne chaque type générique introduit pour la méthode. Ainsi, il est permis d'écrire une méthode Dummy<T>(Int: Integer): Integer, qui n'a donc aucun paramètre formel dont le type est le paramètre générique T. En C++, par exemple, ça ne passerait pas.
Côté implémentation de la méthode, c'est tout à fait similaire aux classes. Il faut répéter les chevrons et les noms des types génériques, mais pas leurs contraintes éventuelles. Cela donne donc :
class
function
TArrayUtils.Min<T>(const
Items: array
of
T;
const
Comparer: IComparer<T>): T;
var
I: Integer
;
begin
if
Length(Items) = 0
then
raise
Exception.Create('No items in the array'
);
Result := Items[Low(Items)];
for
I := Low(Items)+1
to
High(Items) do
if
Comparer.Compare(Items[I], Result) < 0
then
Result := Items[I];
end
;
Rien de bien exceptionnel donc ;-)
VIII-B. La surcharge et les contraintes▲
Profitons de ce bel exemple pour revoir nos contraintes, et proposer une version surchargée pour les types d'éléments qui supportent l'interface IComparable<T> (cette interface est définie dans System.pas).
Et avant ça, ajoutons une autre version surchargée qui prend une référence de routine de type TComparison<T>. Rappelez-vous qu'on peut facilement « transformer » un call-back TComparison<T> en une interface IComparer<T> avec TComparer<T>.Construct.
Vous pouvez observer l'utilisation de la méthode CompareTo sur le paramètre Left. Ceci n'est bien sûr possible que parce que, dans cette surcharge, le type T est contraint à supporter l'interface IComparable<T>.
type
TArrayUtils = class
public
class
function
Min<T>(const
Items: array
of
T;
const
Comparer: IComparer<T>): T; overload
; static
;
class
function
Min<T>(const
Items: array
of
T;
const
Comparison: TComparison<T>): T; overload
; static
;
class
function
Min<T: IComparable<T>>(
const
Items: array
of
T): T; overload
; static
;
end
;
class
function
TArrayUtils.Min<T>(const
Items: array
of
T;
const
Comparison: TComparison<T>): T;
begin
Result := Min<T>(Items, TComparer<T>.Construct(Comparison));
end
;
class
function
TArrayUtils.Min<T>(const
Items: array
of
T): T;
var
Comparison: TComparison<T>;
begin
Comparison :=
function
(const
Left, Right: T): Integer
begin
Result := Left.CompareTo(Right);
end
;
Result := Min<T>(Items, Comparison);
end
;
Remarquez l'appel à Min<T> : il est indispensable de spécifier à l'appel aussi le (ou les) type réel utilisé. Ceci contraste avec d'autres langages comme le C++.
Maintenant, nous voulons proposer une quatrième version surchargée, toujours avec uniquement le paramètre Items, mais avec un paramètre T non contraint. Cette version devrait utiliser TComparer<T>.Default.
Mais ceci n'est pas possible ! Car, bien que les contraintes sur les types changent, les paramètres (arguments) sont les mêmes. Donc les deux versions surchargées sont totalement ambigües ! Ainsi, la déclaration supplémentaire suivante échouera à la compilation :
type
TArrayUtils = class
public
class
function
Min<T>(const
Items: array
of
T;
const
Comparer: IComparer<T>): T; overload
; static
;
class
function
Min<T>(const
Items: array
of
T;
const
Comparison: TComparison<T>): T; overload
; static
;
class
function
Min<T: IComparable<T>>(
const
Items: array
of
T): T; overload
; static
;
class
function
Min<T>(
const
Items: array
of
T): T; overload
; static
; // Erreur de compilation
end
;
Il faut donc faire un choix : abandonner l'un ou l'autre, ou utiliser un autre nom. Et comme, jusqu'à ce que les types de base comme Integer supportent l'interface IComparable<T>, vous risquez d'utiliser aussi souvent l'un que l'autre, il va falloir opter pour l'autre nom ;-)
type
TArrayUtils = class
public
class
function
Min<T>(const
Items: array
of
T;
const
Comparer: IComparer<T>): T; overload
; static
;
class
function
Min<T>(const
Items: array
of
T;
const
Comparison: TComparison<T>): T; overload
; static
;
class
function
Min<T: IComparable<T>>(
const
Items: array
of
T): T; overload
; static
;
class
function
MinDefault<T>(
const
Items: array
of
T): T; static
;
end
;
class
function
TArrayUtils.MinDefault<T>(const
Items: array
of
T): T;
begin
Result := Min<T>(Items, IComparer<T>(TComparer<T>.Default
));
end
;
Pourquoi le transtypage explicite en IComparer<T> de Default qui est pourtant manifestement déjà un IComparer<T> ? Parce que les références de routine et les surcharges ne font pas encore très bon ménage, et le compilateur a l'air de s'emmêler les pinceaux. Sans le transtypage, la compilation ne passe pas…
VIII-C. Des ajouts pour TList<T>▲
Si la classe TList<T> est une belle innovation, il n'en demeure pas moins qu'elle pourrait contenir plus de méthodes d'intérêt pratique.
Voici donc par exemple une implémentation de la méthode .NET FindAll pour TList<T>. Cette méthode a pour but de sélectionner une sous-liste à partir d'une fonction prédicat. Ce qu'on appelle fonction prédicat est une routine de call-back qui prend en paramètre un élément de la liste, et renvoie True s'il faut le sélectionner. On définit donc un type référence de routine TPredicate<T> comme suit :
unit
Generics.CollectionsEx;
interface
uses
Generics.Collections;
type
TPredicate<T> = reference to
function
(const
Value: T): Boolean
;
Ensuite, comme malheureusement il semble impossible d'écrire un class helper pour une classe générique, nous allons écrire une méthode de classe FindAll<T> qui va faire cela. Puisqu'on est privé de class helper, on va en moins en profiter pour être plus général, et travailler sur un énumérateur quelconque, avec une surcharge pour un énumérable quelconque.
type
TListEx = class
public
class
procedure
FindAll<T>(Source: TEnumerator<T>; Dest: TList<T>;
const
Predicate: TPredicate<T>); overload
; static
;
class
procedure
FindAll<T>(Source: TEnumerable<T>; Dest: TList<T>;
const
Predicate: TPredicate<T>); overload
; static
;
end
;
implementation
class
procedure
TListEx.FindAll<T>(Source: TEnumerator<T>; Dest: TList<T>;
const
Predicate: TPredicate<T>);
begin
while
Source.MoveNext do
begin
if
Predicate(Source.Current) then
Dest.Add(Source.Current);
end
;
end
;
class
procedure
TListEx.FindAll<T>(Source: TEnumerable<T>; Dest: TList<T>;
const
Predicate: TPredicate<T>);
begin
FindAll<T>(Source.GetEnumerator, Dest, Predicate);
end
;
end
.
On peut s'en servir comme ceci :
Source := TList<Integer
>.Create;
try
Source.AddRange([2
, -9
, -5
, 50
, 4
, -3
, 7
]);
Dest := TList<Integer
>.Create;
try
TListEx.FindAll<Integer
>(Source, Dest, TPredicate<Integer
>(
function
(const
Value: Integer
): Boolean
begin
Result := Value > 0
;
end
));
for
Value in
Dest do
WriteLn(Value);
finally
Dest.Free;
end
;
finally
Source.Free;
end
;
À nouveau, le transtypage est nécessaire à cause des surcharges. C'est assez déplorable, mais c'est comme ça. Si vous préférez ne pas avoir de transtypage, utilisez des noms différents, ou débarrassez-vous d'une des deux versions.
Il ne tient qu'à vous de compléter cette classe avec d'autres méthodes de ce genre :-)