Le programmeur au travail

Collections .NET : Premiers pas avec C5

Ted Neward

 

Ted NewardJ'ai une confession à faire.

Le jour, j'exerce tranquillement la profession de développeur .NET pour Neudesic LLC, un cabinet de conseil. Mais la nuit, une fois que ma femme et mes fils sont endormis, je me faufile hors de la maison, avec mon ordinateur portable sous le bras, puis je m'enfuis vers ma cachette secrète (un restaurant Denny's de la 148e rue) et je… j'écris du code Java.

Oui mes amis, je mène une double vie : je suis développeur .NET et développeur Java ou, pour être plus exact, JVM (Java Virtual Machine, machine virtuelle Java). Grâce à cette double vie, je vois notamment quelles excellentes idées de Microsoft .NET Framework peuvent être appliquées à la JVM. Je fais plus particulièrement référence aux attributs personnalisés, que la JVM a adoptés dans Java5 (plus ou moins en 2005) sous le nom d'« annotations ». Mais l'inverse est également vrai : En fait, la JVM parvenait à faire un certain nombre de choses que le CLR et la BCL (Base Class Library, bibliothèque de classes de base) .NET ne faisaient pas (ou tout au moins pas aussi bien, au cas où vous vous sentiriez un peu sur la défensive). Je pense notamment aux collections, qui se trouvent au cœur de la BCL .NET.

Les collections : une critique

Le défaut des collections .NET vient en partie du fait que l'équipe BCL a dû écrire les choses étranges à deux reprises : une première fois pour la version du .NET Framework 1.0/1.1, avant que les génériques ne soient disponibles, et une seconde fois pour le .NET Framework 2.0, après l'intégration des génériques dans le CLR, car les collections sans versions fortement typées semblent vraiment étranges. Cela signifiait irrémédiablement que l'une de ces versions allait probablement tomber dans l'oubli, en fonction des améliorations ou des ajouts futurs effectués au niveau de la bibliothèque. Java a essentiellement esquivé ce problème en « remplaçant » les versions sans génériques par des versions avec génériques, ce qui était uniquement possible en raison de la façon dont Java traitait les génériques, mais je ne détaillerai pas ce point ici. De plus, outre les améliorations apportées grâce à LINQ dans Visual Studio 2008 et C# 3.0, la bibliothèque de collections n'a jamais été très appréciée après la version 2.0, qui se contentait plus ou moins de réimplémenter les classes System.Collections dans un nouvel espace de noms (System.Collections.Generic, ou SCG) des versions fortement typées.

Mais surtout, il semble que les concepteurs des collections .NET aient plus cherché à lancer quelque chose de pratique et d'utile dans le cadre de la version 1.0 qu'à réfléchir profondément à la conception des collections et à leurs possibilités d'extension. Il s'agit de l'un des domaines dans lesquels le .NET Framework a véritablement atteint le même niveau que le monde Java (à mon avis, involontairement). Lors de son lancement, Java 1.0 comprenait un ensemble de collections utilitaires de base. Celles-ci présentaient toutefois quelques défauts de conception (le plus flagrant d'entre eux étant la décision de faire étendre directement la classe Vector, globalement une liste de tableaux, par la classe Stack, une collection dernier entré, premier sorti). Après le lancement de Java 1.1, quelques ingénieurs de Sun Microsystems ont travaillé intensivement pour réécrire les classes de collections, désormais connues sous le nom de Collections Java, qui ont été lancées avec Java 1.2.

Quoi qu'il en soit, il est plus que temps pour .NET Framework de repenser ses classes de collections, idéalement dans un cadre essentiellement compatible avec les classes SCG existantes. Fort heureusement, les chercheurs de l'université informatique de Copenhague au Danemark ont créé un successeur digne de ce nom qui vient compléter les classes SCG : une bibliothèque qu'ils qualifient de classes de collection génériques de Copenhague pour C# (Copenhagen Comprehensive Collection Classes for C#), ou C5 pour faire court.

Logistique de C5

Tout d'abord, C5 se trouve sur le Web, à l'adresse itu.dk/research/c5, si vous souhaitez consulter un historique de la version ou obtenir un lien vers un livre (PDF) consacré à C5, bien que cette documentation ne date pas de la toute dernière version. En parallèle, C5 est également disponible via NuGet, avec la commande Install-Package (désormais omniprésente). Il vous suffit pour cela de taper « Install-Package C5 ». Notez que C5 est écrit de façon à être disponible pour Visual Studio et Mono. Lorsque NuGet installe le package, il ajoute des références à l'assembly C5.dll et à l'assembly C5Mono.dll. Ces deux assemblys sont redondants, veillez donc à supprimer celui dont vous n'avez pas besoin. Afin de permettre l'étude des collections C5 à travers une série de tests d'exploration, j'ai créé un projet de test Visual C# et j'ai ajouté C5 à ce projet. Outre ce point, les deux instructions « using » sont la seule modification importante à noter. D'ailleurs, la documentation de C5 suppose également ce changement :

using SCG = System.Collections.Generic;
using C5;

La présence de l'alias se justifie simplement : C5 « réimplémente » quelques interfaces et classes dont le nom est identique dans la version SCG. Grâce à l'alias des anciens éléments, nous pouvons encore accéder à ces derniers, mais sous un préfixe très court (par exemple, IList<T> est la version C5, tandis que SCG.IList<T> correspond à la version « standard » de SCG).

D'ailleurs, au cas où l'on vous poserait la question, C5 est sous licence open source MIT, ce qui vous laisse bien plus de possibilités de modification ou d'amélioration de certaines classes C5 que sous une licence publique générale GNU (GPL) ou une licence publique générale limitée GNU (LGPL).

Présentation de la conception de C5

Si vous observez l'approche adoptée pour la conception de C5, elle vous semblera similaire au style SCG, en ce sens que les collections sont divisées en « niveaux » : une couche d'interface qui décrit l'interface et le comportement attendus d'une collection donnée, et une couche d'implémentation qui fournit le code de soutien réel pour une ou plusieurs interfaces souhaitées. Les classes SCG reposent sur une idée similaire, mais elles ne la suivent pas très bien dans certains cas. Par exemple, nous n'avons aucune souplesse en termes d'implémentation de SortedSet<T> (c'est-à-dire le choix entre l'interface basée sur un tableau, basée sur une liste liée ou basée sur le hachage, chaque option présentant des caractéristiques différentes en ce qui concerne les performances d'insertion, de parcours, etc.). Dans certains cas, il manque simplement certains types de collection aux classes SCG, par exemple une file d'attente circulaire (dans laquelle, lorsque le dernier élément de la file est parcouru, l'itération revient à nouveau au début de la file), ou une simple collection « bag », qui n'offre aucune fonctionnalité particulière si ce n'est qu'elle contient des éléments et permet ainsi d'éviter les surcharges inutiles liées au tri, à l'indexation, etc.

J'admets que cela n'est pas une grande perte pour le développeur .NET moyen. Mais dans la plupart des applications, lorsque les performances commencent à être au cœur des préoccupations, il devient encore plus important de choisir la classe de collection correspondant au problème en cours. Cette collection sera-t-elle établie une seule fois et parcourue fréquemment ? Ou bien s'agit-il d'une collection qui sera souvent ajoutée ou supprimée, mais rarement parcourue ? Si cette collection est au cœur de la fonctionnalité d'une application (ou de l'application elle-même), la différence entre les deux utilisations précédemment mentionnées pourrait susciter l'une ou l'autre réaction : soit « Waouh, cette application est laborieuse ! », soit « Bon, l'utilisateur l'a bien aimée, mais il a simplement trouvé qu'elle était trop lente ».

Ensuite, l'un des principes de base sur lesquels repose C5 est que les développeurs doivent « coder les interfaces et non les implémentations ». C'est pourquoi cette bibliothèque offre plus de dix interfaces différentes qui décrivent ce qui doit être fourni pas la collection sous-jacente. ICollection<T> est à la base de tout et garantit un comportement de collection de base, mais cela nous mène aux interfaces IList<T>, IIndexed<T>, ISorted<T> et ISequenced<T>, pour commencer seulement. Voici la liste complète des interfaces, de leurs relations avec d'autres interfaces et de ce qu'elles vous apportent globalement :

  • Avec SCG.IEnumerable<T>, les éléments peuvent être énumérés. Toutes les collections et tous les dictionnaires sont énumérables.
  • L'interface IDirectedEnumerable<T> est énumérable et peut être inversée, ce qui signifie que les éléments peuvent être énumérés dans l'ordre inverse.
  • ICollectionValue<T> est une valeur de collection. Elle ne prend pas en charge les modifications, est énumérable, connaît le nombre d'éléments qu'elle contient et peut les copier dans un tableau.
  • IDirectedCollectionValue<T> est une valeur de collection qui peut être inversée de façon à devenir une valeur de collection à l'envers.
  • IExtensible<T> est une collection à laquelle il est possible d'ajouter des éléments.
  • IPriorityQueue<T> est une collection extensible dont les éléments supérieur et inférieur peuvent être trouvés (et supprimés) facilement.
  • ICollection<T> est une collection extensible dont les éléments peuvent également être supprimés.
  • ISequenced<T> est une collection dont les éléments apparaissent selon une séquence particulière (déterminée par l'ordre d'insertion ou celui des éléments).
  • IIndexed<T> est une collection séquencée dont les éléments sont accessibles par index.
  • ISorted<T> est une collection séquencée dans laquelle les éléments apparaissent dans l'ordre croissant. Les comparaisons entre les éléments déterminent leur séquence. Elle peut retrouver efficacement le prédécesseur ou le successeur (dans la collection) d'un élément donné.
  • IIndexedSorted<T> est une collection indexée et triée. Elle peut déterminer efficacement combien d'éléments sont supérieurs ou égaux à un élément donné x.
  • IPersistentSorted<T> est une collection triée dont il est possible de réaliser efficacement un instantané, c'est-à-dire une copie en lecture seule à laquelle les mises à jour de la collection d'origine ne seront pas appliquées.
  • IQueue<T> est une file d'attente FIFO (first-in-first-out, premier entré, premier sorti) qui prend également en charge l'indexation.
  • IStack<T> est une pile LIFO (last-in-first-out, dernier entré, dernier sorti).
  • IList<T> est une collection indexée et donc séquencée, dans laquelle l'ordre des éléments est déterminé par des insertions et des suppressions. Elle est dérivée de SCG.IList<T>.

C5 propose un certain nombre d'implémentations, dont les files d'attente circulaires, les listes soutenues par des tableaux et des listes liées, mais également les listes dans un tableau haché et les listes liées de hachage, les tableaux encapsulés, les tableaux triés, les ensembles et les sacs basés sur l'arborescence, etc.

Style de codage C5

Fort heureusement, C5 ne requiert aucun changement radical dans votre style de codage et prend même en charge toutes les opérations LINQ (elle repose en effet sur les interfaces SCG, dont sont issues les méthodes d'extension LINQ), ce qui est encore mieux. C'est pourquoi vous pouvez dans certains cas ajouter une collection C5 lors de la création sans modifier aucunement le code qui l'entoure. La figure 1 illustre ce cas de figure.

Figure 1 Débuter avec C5

// These are C5 IList and ArrayList, not SCG
IList<String> names = new ArrayList<String>();
names.AddAll(new String[] { "Hoover", "Roosevelt", "Truman",
  "Eisenhower", "Kennedy" });
// Print list:
Assert.AreEqual("[ 0:Hoover, 1:Roosevelt, 2:Truman, 3:Eisenhower," +
  " 4:Kennedy ]", names.ToString());
// Print item 1 ("Roosevelt") in the list
Assert.AreEqual("Roosevelt", names[1]);
Console.WriteLine(names[1]);
// Create a list view comprising post-WW2 presidents
IList<String> postWWII = names.View(2, 3);
// Print item 2 ("Kennedy") in the view
Assert.AreEqual("Kennedy", postWWII[2]);
// Enumerate and print the list view in reverse chronological order
Assert.AreEqual("{ Kennedy, Eisenhower, Truman }",
  postWWII.Backwards().ToString());

Même sans avoir jamais consulté la moindre documentation sur C5, vous pouvez aisément comprendre ce qui se passe dans ces exemples.

Implémentation de C5 plutôt que de la collection SCG

Ce n'est que la partie émergée de l'iceberg avec C5. Mon article suivant sera consacré à certains exemples pratiques de l'utilisation de C5 au lieu des implémentations de collection SCG. Je présenterai également certains des avantages de ce choix. Je vous encourage cependant à ne pas attendre : tapez la commande NuGet, accédez à C5 et commencez votre propre exploration, vous trouverez largement de quoi vous amuser avant mon prochain article.

Bon codage !

Ted Neward est consultant en architecture chez Neudesic LLC. Auteur de plus de 100 articles, il a rédigé ou corédigé plus d'une dizaine d'ouvrages, y compris « Professional F# 2.0 » (Wrox, 2010). Il est MVP F# et un expert reconnu en Java. Il intervient en outre lors de conférences Java et .NET dans le monde entier. Il accepte des missions de conseil et de tutorat. Vous pouvez le contacter à l'adresse ted@tedneward.com ou Ted.Neward@neudesic.com si vous souhaitez qu'il vienne travailler avec votre équipe. Il tient un blog, dont l'adresse est blogs.tedneward.com, et vous pouvez le suivre sur Twitter, à l'adresse twitter.com/tedneward.

Merci à l'expert technique suivant d'avoir relu cet article : Immo Landwerth