C# version 2 : L'intégration des Templates
Les Templates sont un mécanisme permettant de donner une représentation
générique et homogène d'une collection d'objets ou d'une classe donnée, qui
prendra le nom de classe "polymorphe" ou "générique". Cette
notion est présente dans plusieurs langages objets et particulièrement en C++
qui a énormément contribué à son
adoption. Si les Templates revêt aujourd'hui
une importance capitale pour C# mais aussi d'une certaine mesure pour Java,
c'est en partie dû au caractère néfaste de la programmation faiblement typée.
La généricité à travers la classe Object nuit à la robustesse du code et
fait office de porte ouverte à toutes les manipulations hasardeuses. Ainsi, il
n'existe aucun moyen simple pour éviter de mélanger des objets torchons et des
objets serviettes dans une même collection car tous les objets héritent de
cette fameuse super classe Object.
Dans ce cas, comment faire pour restreindre et assurer un même type pour un ensemble de données ? C'est la question à laquelle nous allons essayer de répondre à travers cet article.
DotNetGuru est allé à la rencontre de Don Syme (photo ci-contre), chercheur chez Microsoft dans les laboratoires de Cambridge en Angleterre. A travers un document intitulé "Conception et implémentation des types génériques pour la CLR .NET (document pdf)", Don nous donne sa vision du futur de C#. Nous avons essayé d'en savoir un peu plus en nous appuyant sur les premières réflexions déjà formalisées du coté de Java : « Des Templates en Java » par Rémi Forax (1998) que nous vous conseillons de lire absolument.
Les templates ou "polymorphisme
paramétré" permettent de contraindre l'utilisation d'un type donné de
manière totalement générique au niveau du langage. Cette approche privilégie le
typage statique, plus sûr, au typage dynamique, moins efficace et plus dangereux.
Voyons un exemple concret. Vous disposez dans l’exemple suivant d'un code C#
représentant une pile d'éléments. Vous remarquerez que la classe Stack
type l'ensemble de ces éléments à l'aide d'Object. Rien n'interdit donc
à un utilisateur de réaliser l'opération Stack.Push(new Serviette()) et Stack.Push(new
Torchon()). Ces deux opérations sont autorisées et ni le compilateur ni la
CLR ne vous avertiront de quoi que ce soit, jusqu'au moment où l'utilisateur
prendra les Torchons pour des Serviettes (d’où la fameuse expression « prendre
les lanternes pour des torchons » euh…, pas sûr, mais quelque chose dans
le style ;-)).
|
Classe Stack en C# sans Template |
|
Remi Forax dispose d'une très belle formule pour illustrer ce genre de choses : "La différence fondamentale entre ces deux approches est qu'un template de pile permet de générer n'importe quelle pile, alors qu'une pile d'Object est une pile de n'importe quoi." Sacré Remi …
De plus, une autre forme, celle-ci plus
sournoise, des dangers provoqués par l'approche actuelle, est la dégradation des
performances dûe à l'utilisation du Boxing et UnBoxing afin de
transformer un type primitif en type Object. Que ce soit en C# ou en Java, les
deux langages imposent des conversions ou transformations (cast) implicites ou
explicites qui s'avèrent pénalisantes à l'exécution. Exemple :
Stack s = new
Stack()
s.Push(1); //
Boxing
s.Push(2);
int n = (int) (s.Pop()) + (int)
(s.Pop()) ; // UnBoxing
Stack s = new
Stack()
s.Push(new
Integer(1)); // Boxing
s.Push(new
Integer(2));
int n =
s.Pop().intValue() + s.Pop().intValue() ; // UnBoxing
Le Boxing impose la création d'un objet qui encapsulera le type de base, inutile de préciser que cette opération multipliée par 1.000 ou 10.000 entiers implique la création d'autant d'objets qui monopolisent inutilement de la mémoire. Bref, cette opération peut-être évitée lorsqu’on manipule le type réel primitif.
Si les Templates n'existent pas aujourd'hui dans C#, cela ne signifie pas qu'il n'est pas possible de trouver des solutions élégantes de contournement. Il est possible de protéger une structure de données à l'aide d'un Wrapper qui jouera le rôle de barrière de protection. Si les problèmes évoqués précédemment subsistent, ils sont déportés dans la classe intermédiaire qui assure l'uniformité des types insérés. Le code suivant nous illustre le principe.
class
Stack
{
private Object[] items;
private int nitems;
Stack()
{ nitems = 0; items = new Object[50]; }
Object
Pop()
{
if (nitems == 0) throw
new EmptyException();
return items[--nitems];
}
void Push(Object item)
{
(...)
return items[nitems++];
}
}
class
StackMyType
{
private Stack s = new
Stack() ;
MyType
Pop()
{
return (MyType) s.Pop() ;
}
void Push(MyType item)
{
s.Push(item);
}
(Main.cs)
StackMyType s = new StackMyType()
s.Push(new MyWrongType()); //
Compiler error
s.Push(new MyType()); // Ok
Malheureusement, ce principe ne couvre pas la généricité des classes ou interfaces, ni des ValueType (structs et enums) . C'est pourquoi, un mécanisme intégré au langage doit être mis en oeuvre afin d'assurer un typage statique permettant de lever les incohérences intervenant dans le code dès la phase de compilation.
La solution implémentée par Don lors de ces recherches est axée sur plusieurs points bien précis :
L'expansion dynamique et le partage de code : L'instanciation des classes génériques est prise en charge par la CLR. Si nécessaire, une génération dynamique de code sera effectuée en accord avec le compilateur JIT. Enfin, le code pouvant être partagé au niveau de l'implémentation sera mis à disposition des autres classes. Ainsi un type StackMyType pourra utiliser l'implémentation de StackMyOtherType.
La prise en charge des types dans chaque objet : "Pass and Store". Consiste à stocker les informations relatives au type directement dans l'objet. Un paramètre supplémentaire sera toutefois nécessaire lors de l'invocation des méthodes génériques afin de spécifier le type réel.
L'optimisation de la gestion des types génériques. Les valeurs primitives sont stockées telles quelles, ce qui améliore considérablement les performances liées aux opérations de Boxing/Unboxing.
Le code suivant nous illustre le principe :
|
Classe Stack en C# sans Template |
Classe Stack en C# avec Template |
|
EmptyException();
|
La généricité ne s'arrête pas là dans le modèle que préconise Don. Suivant son principe, il est possible de définir des interfaces génériques de la manière suivante :
interface IDictionary<K,D>
{
D Lookup(K);
...
}
class
Dictionary<K,D> : IDictionary<K,D>
{
D Lookup(K);
...
}
Dictionary<String,String>
Un dictionnaire comprenant des clés génériques pointant sur des entrées génériques ! Remi Forax avait tenté, lui aussi, de décrire de telles structures génériques.
La problématique est même poussée à l'extrême avec la notion de méthodes génériques :
static
void Sort<T>
(T[]) { ... }
int[]
x = { 5,4,3,2 };
Sort(x);
En pratique, les méthodes génériques sont juste une famille de méthodes indexées par un type donnée. Il n'est pas sûr que Microsoft accepte d'aller jusqu'à ce niveau d'abstraction pour les prochaines versions de C#, mais sait-on jamais ...
Et pour finir, la cerise sur le gâteau avec, accrochez-vous, les Contraintes explicites génériques.
interface
IComparable<T> { static int Compare(T,T); }
class
BinaryTree<T : IComparable<T> >
void insert(T x)
{
switch
(x.Compare(y))
{... }
}
T y;
Tree<T> left;
Tree<T> right;
}
Cette notion permet de contraindre un héritage par rapport à un type générique donné. Ainsi, un arbre binaire ne pourra être comparé que par des interfaces en accord avec le type « arbre ».
Pour résumer, Don propose de paramétrer les classes, les interfaces, les structures, les méthodes et les déléguées (rien que ça!). Mais aussi de faire appel à l'opérateur "is" afin de comparer deux types génériques de la manière suivante :
// Comparaison par rapport au type générique
if (x is Set<string>) { ... }.
Pour ce faire, la CLR sera sollicitée pour assurer le chargement dynamique des types, mais le compilateur JIT sera aussi revu et corrigé afin d'intégrer la notion de types génériques. Autant dire que le challenge est considérable et il n'est pas certain que toutes ces fonctionnalités soient présentes dans la prochaine version de C# vu les changements que ceux-ci impliquent.
Au vu de ce qui a été évoqué précédemment, vous imaginez bien que les implications concernant le code MSIL sont importantes. Don préconise de remplacer l'ensemble des références à la classe System.Object par le type générique <T> utilisé. Ce sera ensuite à la CLR de remplacer dynamiquement ces types par les types concrets à travers une génération dynamique de code ou tout autre mécanisme de cet ordre. (PS : Vous comprenez pourquoi DotNetGuru insiste sur la génération dynamique de code, elle intervient à tous les niveaux).
Pour ce faire, il faudra absolument revoir le mécanisme d'interprétation de la CLR et de compilation JIT car celui-ci se base sur un code fixe et fortement typé. Le Verifier aura aussi la lourde tâche d'analyser un code qui s'auto construit en fonction des valeurs des Templates. Inutile de dire que tout cela aura un effet non négligeable sur l'architecture existante.

Les Templates ont été mis de coté dans la version actuelle de C# car cette fonctionnalité était jugée non prioritaire par Microsoft dans un premier temps, qui s’attachait d’abord à stabiliser et à promouvoir le langage. Demain, les développeurs pourront choisir entre continuer à utiliser ArrayList ou préférer ArrayList<T> en bénéficiant de l’apport des types génériques. Ce sera ainsi une brique de plus à l'édifice déjà riche de C#. Concernant Java, vous trouverez sur le lien suivant un article de JavaWorld annonçant une intégration prochaine des Templates dans le JDK 1.5 (Merci Regis LOWE pour l'info) Article Template/Java JavaWorld , A Suivre donc ...
Auteur : Sami Jaber
Copyright : DotNetGuru Ó 2002
Slides présentés par Don Syme au cours de Dev Days (A venir ...)
Rémi Forax (Templates en Java)