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.

Que sont les templates (ou types paramétrés) ? 

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

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++];
  }
}
 

 

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

 

C#

 

Stack s = new Stack()

s.Push(new Integer(1)); // Boxing

s.Push(new Integer(2));

int n = s.Pop().intValue() + s.Pop().intValue() ; // UnBoxing

 

Java

 

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.

Implémenter du code générique dans C# aujourd'hui   

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 C# Version 2 de Don Syme

La solution implémentée par Don lors de ces recherches est axée sur plusieurs points bien précis :

Le code suivant nous illustre le principe :

 

Classe Stack en C# sans Template

Classe Stack en C# avec Template

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 Stack<T> {
  private T[] items;
  private int nitems;
 
  Stack() { nitems = 0; items = new T[50]; }

  T Pop() {
    if (nitems == 0) throw new     

                                  EmptyException();
    return items[--nitems];
}

  void Push(T item) {
    if (items.Length == nitems) {
      T[] temp = items;
      items = new T[nitems*2];
      Array.Copy<T>(temp, items, nitems);
    }
    items[nitems++]
= item;
  }
}

 

 

 

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.

 Implications au niveau du code MSIL

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.

 

 

Conclusion

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

Ressources

Slides présentés par Don Syme au cours de Dev Days (A venir ...)

Rémi Forax (Templates en Java)