Partager via


Le programmeur au travail

Collections .NET, 2e partie : Utilisation de C5

Ted Neward

 

 

Ted NewardJe suis ravi de vous retrouver.

Dans la première partie de cette série, j'ai brièvement présenté la bibliothèque Copenhagen Comprehensive Collection Classes for C#, ou C5, un ensemble de classes conçues pour compléter (si ce n'est remplacer) les classes System.Collections fournies avec la bibliothèque runtime du Microsoft .NET Framework. Les deux bibliothèques se rejoignent sur certains points, en partie parce que C5 veut suivre un grand nombre d'idiomes utilisés par la bibliothèque de classes .NET Framework, et en partie parce qu'il existe un nombre limité de façons dont il est possible de représenter un type de collection donné. Il est difficile d'imaginer une collection indexée, par exemple un dictionnaire ou une liste, ne prenant pas en charge la syntaxe du langage pour les propriétés indexées : l'opérateur « [] » en C# et l'opérateur « () » en Visual Basic. Toutefois, là où les collections FCL sont utilitaires, les collections C5 vont légèrement ou bien plus loin et c'est ce qui nous intéresse ici.

Notez qu'il existe très certainement des différences de performances entre les deux bibliothèques que les défenseurs ou détracteurs de chacune d'entre elles ne manqueront pas de souligner rapidement (le manuel de la collection C5 aborde notamment certaines des implications en matière de performances. Cela étant dit, je reste aussi loin que possible des tests de performances qui ne prouvent généralement qu'une seule chose : pour tel cas ou tel ensemble de cas, quelqu'un a réussi à exécuter l'une des deux plus rapidement que l'autre. Or rien ne prouve que cela sera vrai dans tous les cas de figure. Cela ne signifie en aucun cas que tous les tests de performances sont inutiles, mais simplement que le contexte a son importance. Les lecteurs sont fortement encouragés à prendre leurs propres scénarios, à les soumettre à des tests et à comparer les deux, simplement pour vérifier s'il existe une différence marquée dans ces cas spécifiques.

Implémentations

Commençons par observer rapidement les différentes implémentations de collections fournies par C5. Comme nous l'avons déjà vu la dernière fois, les développeurs qui utilisent C5 n'ont généralement pas besoin de se préoccuper de l'implémentation utilisée, sauf lorsqu'ils s'interrogent sur l'implémentation à créer (le reste du temps, la collection doit être référencée par type d'interface). Si vous souhaitez consulter une description des types d'interface, lisez l'article précédent de la série, sur msdn.microsoft.com/magazine/jj883961, ou la documentation C5 sur bit.ly/UcOcZH.) Voici les implémentations :

  • CircularQueue<T> implémente IQueue<T> et IStack<T> afin de fournir la sémantique premier entré, premier sorti d'IQueue<T> (via Enqueue et Dequeue) ou bien la sémantique dernier entré, premier sorti d'IStack<T> (via Push et Pop), avec une liste liée en soutien. Sa capacité augmente en fonction des besoins.
  • ArrayList<T> implémente IList<T>, IStack<T> et IQueue<T>, avec un tableau en soutien.
  • LinkedList<T> implémente IList<T>, IStack<T> et IQueue<T>, à l'aide d'une liste de nœuds doublement liée.
  • HashedArrayList<T> implémente IList<T>, avec un tableau en soutien, mais gère également un tableau de hachage en interne afin de trouver rapidement la position d'un élément au sein de la liste. En outre, elle n'autorise pas les doublons dans la liste (étant donné qu'ils nuiraient à la recherche dans la table de hachage).
  • HashedLinkedList<T> implémente IList<T>, avec une liste liée en soutien, et elle utilise une table de hachage interne pour optimiser les recherches, comme son homologue soutenu par un tableau.
  • WrappedArray<T> implémente IList<T> et s'ajuste autour d'un tableau unidimensionnel. Cette classe présente l'avantage de « décorer » simplement le tableau, en accélérant largement l'obtention de la fonctionnalité C5, par opposition au processus de copie des éléments hors du tableau et dans ArrayList<T>.
  • SortedArray<T> implémente IIndexedSorted<T>, ce qui signifie que la collection peut être indexée et triée (nous reviendrons sur ce point dans quelques instants). Elle garde ses éléments triés et n'autorise pas les doublons.
  • TreeSet<T> implémente IIndexedSorted<T> et IPersistedSorted<T>. Elle est en outre soutenue par une arborescence rouge-noire équilibrée, ce qui convient parfaitement aux insertions, suppressions et au tri. Comme tous les ensembles, elle n'autorise pas les doublons.
  • TreeBag<T> implémente IIndexedSorted<T> et IPersistedSorted<T>, est soutenue par une arborescence rouge-noire équilibrée, mais est essentiellement un « conteneur » (parfois nommé « multiensemble »), ce qui signifie qu'elle autorise les doublons.
  • HashSet<T> implémente IExtensible<T> et soutient l'ensemble (ce qui signifie absence de doublons) à l'aide d'une table de hachage avec un chaînage linéaire. Cela signifie que les recherches seront rapides, mais les modifications un peu moins.
  • HashBag<T> implémente IExtensible<T> et soutient le conteneur (ce qui signifie que les doublons sont autorisés) à l'aide d'une table de hachage avec un chaînage linéaire. Là encore, cela permet des recherches rapides.
  • IntervalHeap<T> implémente IPriorityQueue<T>, à l'aide d'un tas d'intervalle stocké comme tableau de paires, ce qui facilite les extractions à partir de l'extrémité « max » ou « min » de la file d'attente prioritaire.

Il existe quelques implémentations supplémentaires et le manuel et les documentations C5 contiennent d'autres détails si vous êtes intéressé. Toutefois, outre les implications en matière de performances, il est surtout essentiel de savoir quelles implémentations implémentent quelles interfaces afin d'avoir une bonne idée de ces éléments au moment d'en choisir un à instancier. Vous pouvez toujours l'appliquer à une autre implémentation par la suite, en supposant que vous suiviez la directive de conception C5 selon laquelle les collections doivent être référencées par les interfaces plutôt que par leur type d'implémentation.

Fonctionnalité

Si C5 était une simple collection d'implémentations de collections de plus grande taille, elle serait intéressante, mais probablement pas suffisamment pour soulever un intérêt significatif ou faire l'objet de discussions. Fort heureusement, elle offre aux développeurs quelques nouvelles fonctionnalités qui méritent d'être au cœur des discussions.

Vues Parmi d'autres points intéressants, la bibliothèque C5 introduit notamment la notion de « vues »: des sous-collections d'éléments provenant de la collection source et qui ne sont pas des copies, mais sont soutenues par la collection d'origine. C'est ce que faisait le code de mon article précédent, dans le test d'exploration. Consultez la figure 1 pour comprendre la création des vues sur une collection.

Figure 1 Création de vues sur une collection

[TestMethod]
public void GettingStarted()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });
  // 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]);
}

La vue est soutenue par la liste d'origine, ce qui signifie que si cette liste change pour quelque raison que ce soit, la vue associée sera également touchée. Observez la figure 2 pour découvrir comment les vues sont potentiellement mutables.

Figure 2 Les vues sont potentiellement mutables

[TestMethod]
public void ViewExploration()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Washington", "Adams", "Jefferson",
      "Hoover", "Roosevelt", "Truman",
      "Eisenhower", "Kennedy" });
  IList<String> postWWII = names.View(4, names.Count - 4);
  Assert.AreEqual(postWWII.Count, 4);
  IList<String> preWWII = names.View(0, 5);
  Assert.AreEqual(preWWII.Count, 5);
  Assert.AreEqual("Washington", preWWII[0]);
  names.Insert(3, "Jackson");
  Assert.AreEqual("Jackson", names[3]);
  Assert.AreEqual("Jackson", preWWII[3]);
}

Comme le montre ce test, la modification de la liste sous-jacente (« noms ») signifie que les vues définies dessus (dans le cas présent, la vue « preWWII ») ont un contenu qui est également modifié afin que le premier élément de la vue soit désormais « Washington » au lieu de « Hoover ».

Toutefois, lorsque cela s'avère possible, C5 préserve l'intégrité de la vue. Ainsi, par exemple, si l'insertion a lieu au début de la collection (là où C5 peut procéder à l'insertion sans modifier le contenu de la vue « preWWII »), le contenu de la vue reste inchangé :

[TestMethod]
public void ViewUnchangingExploration()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });
  IList<String> preWWII = names.View(0, 2);
  Assert.AreEqual(preWWII.Count, 2);
  names.InsertFirst("Jackson");
  Assert.AreEqual("Jackson", names[0]);
  Assert.AreEqual("Hoover", preWWII[0]);
}

Collections immuables (protégées) Avec l'arrivée des concepts fonctionnels et des styles de programmation, les données immuables et les objets immuables sont devenus le centre d'attention, essentiellement parce que les objets immuables présentent de nombreux avantages par rapport à la programmation simultanée et parallèle, mais aussi parce que de nombreux développeurs trouvent les objets immuables plus faciles à comprendre et à examiner. De ce concept découle celui des collections immuables, c'est-à-dire l'idée que peu importe la nature immuable des objets d'une collection, la collection elle-même est fixe et ne peut pas modifier (ajouter ou supprimer) les éléments qu'elle contient. Remarque : vous pouvez consulter une version préliminaire des collections immuables lancée sur NuGet, sur le blog MSDN Base Class Library (BCL) (bit.ly/12AXD78).

Dans le cadre de C5, les collections immuables sont gérées en instanciant les collections « wrapper » autour de la collection qui contient les données intéressantes. Ces collections sont protégées et utilisées dans le style de modèle standard Decorator :

public void ViewImmutableExploration()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });
  names = new GuardedList<String>(names);
  IList<String> preWWII = names.View(0, 2);
  Assert.AreEqual("Hoover", preWWII[0]);
  names.InsertFirst("Washington");
  Assert.AreEqual("Washington", names[0]);
}

Si quelqu'un tente d'écrire un code qui ajoute ou supprime des éléments de la liste, C5 détourne rapidement le développeur concerné de cette idée : une exception est levée dès que les méthodes « modifiantes » (Add, Insert, InsertFirst, etc.) sont appelées.

Cela offre d'ailleurs une opportunité particulièrement puissante. Dans l'article précédent, j'ai mentionné que l'un des points de conception clés de la bibliothèque C5 était l'idée que les collections devaient uniquement être utilisées via les interfaces. En supposant que les développeurs qui utilisent C5 poursuivent cette idée de conception, il est vraiment simple de garantir aujourd'hui qu'une collection ne sera jamais modifiée par une méthode à laquelle elle est passée (voir la figure 3).

Figure 3 Collections protégées (immuables)

public void IWannaBePresidentToo(IList<String> presidents)
{
  presidents.Add("Neward");
}
[TestMethod]
public void NeverModifiedCollection()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Hoover", "Roosevelt", "Truman","Eisenhower", "Kennedy" });
  try
  {
    IWannaBePresidentToo(new GuardedList<String>(names));
  }
  catch (Exception x)
  {
    // This is expected! Should be a ReadOnlyException
  }
  Assert.IsFalse(names.Contains("Neward"));
 }

Là encore, la méthode IWannaBePresidentToo tente de modifier la collection passée (ce qui est sans doute lié à une mauvaise conception de la part du programmeur qui l'a écrite, mais il existe malheureusement beaucoup d'exemples de code de ce type), une exception est levée.

D'ailleurs, si vous préférez que la collection ne lève aucune exception et échoue silencieusement lors de la tentative de modification (qui est selon moi trop subtile, mais certains développeurs pourraient avoir besoin de cette fonctionnalité), il est relativement simple de créer votre propre version de GuardedArray<T> qui ne lève aucune exception.

Événements Dans certains cas, vous avez besoin d'autoriser les modifications de collections, mais il est alors important de savoir à quel moment une collection est modifiée. Certes, vous pourriez faire tourner un thread indéfiniment sur la collection et comparer le contenu à celui de l'itération précédente. Cependant, non seulement vous gaspilleriez beaucoup de ressources de l'unité centrale, mais ce code est également complexe à écrire et à gérer, ce qui en fait probablement la pire solution de conception possible (et certainement une solution moins riche que si vous utilisiez simplement une collection qui appelle les événements en mode natif). Toutes les collections de C5 offrent la possibilité de retirer les délégués de la collection, d'être appelées lorsque certaines opérations se déroulent par rapport à la collection (voir la figure 4).

Figure 4 « Lorsque je serai président… »

[TestMethod]
public void InaugurationDay()
{
  IList<String> names = new ArrayList<String>();
  names.AddAll(new String[]
    { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });
  names.ItemsAdded +=
    delegate (Object c, ItemCountEventArgs<string> args)
  {
    testContextInstance.WriteLine(
      "Happy Inauguration Day, {0}!", args.Item);
  };
  names.Add("Neward");
  Assert.IsTrue(names.Contains("Neward"));
}

Il est évident que le gestionnaire d'événements peut être écrit comme un lambda. Il est simplement légèrement plus descriptif afin de vous présenter les véritables types d'arguments. Le premier argument est la collection elle-même, comme l'est canon.

Il suffit d'un package NuGet

Toutes les parties de C5 ont été créées autour de FCL .NET (à part l'accent sur les interfaces, que le FCL prend en charge, mais sans véritablement le soutenir semble-t-il), mais ce qui est intéressant avec la bibliothèque C5, c'est qu'elle est faite, testée et n'attend qu'un package d'installation NuGet pour être opérationnelle.

Bon codage !

Ted Neward codirige Neward & Associates LLC. Auteur de plus de 100 articles, il a rédigé et 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