Réimplémentation de LINQ to Objects : Partie 5 - « Empty »

Jon Skeet

Traducteur

Holty Samba SOW

Ce tutoriel est la cinquième partie de la série intitulée Edulinq. Dans cette partie, Jon Skeet nous propose la réimplémentation de l'opérateur « Empty » de Linq to Objects.

Partie précédente : Réimplémentation de LINQ to Objects : Partie 4 - « Range »

Partie suivante : Réimplémentation de LINQ to Objects : Partie 6 - « Repeat »

Commentez l'article : Commentez

- Titre : Réimplémentation de LINQ to Objects : Partie 5 - « Empty »

- Auteur : Jon Skeet

- Traducteur : Holty Samba SOW

- Parution : 29 juin 2013

- Mise à jour : 29 juin 2013

- Public visé :

- Licence : Le contenu de cet article est rédigé par Jon Skeet et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Introduction

- Qu'est-ce que c'est ?

- Qu'allons-nous tester ?

- Voyons l'implémentation !

- Conclusion

- Addenda

- Remerciements

Introduction

En continuant avec les méthodes de non-extension, il est temps éventuellement pour l'opérateur le plus simple autour de LINQ : « Empty ».

Qu'est-ce que c'est ?

« Empty » est générique, une méthode statique avec juste une signature unique et sans paramètres :

public static IEnumerable<TResult> Empty<TResult>()

Il retourne une séquence vide du type approprié. C'est tout ce qu'il fait.

Il y a un seul bit de comportement intéressant : Empty est documenté pour mettre en cache une séquence vide. En d'autres termes, il renvoie une référence à la même séquence vide à chaque fois quand vous l'appelez (pour le même type d'argument, bien sûr).

Qu'allons-nous tester ?

Il y a vraiment seulement deux choses que nous pouvons tester ici :

- la séquence retournée est vide ;

- la séquence retournée est mise en cache par type d'arguments de base.

J'utilise la même approche que pour le Range pour appeler la méthode statique, mais cette fois avec un alias de EmptyClass. Voici les tests :

[Test]
public void EmptyContainsNoElements()
{
using (var empty = EmptyClass.Empty<int>().GetEnumerator())
{
Assert.IsFalse(empty.MoveNext());
}
}

[Test]
public void EmptyIsASingletonPerElementType()
{
Assert.AreSame(EmptyClass.Empty<int>(), EmptyClass.Empty<int>());
Assert.AreSame(EmptyClass.Empty<long>(), EmptyClass.Empty<long>());
Assert.AreSame(EmptyClass.Empty<string>(), EmptyClass.Empty<string>());
Assert.AreSame(EmptyClass.Empty<object>(), EmptyClass.Empty<object>());

Assert.AreNotSame(EmptyClass.Empty<long>(), EmptyClass.Empty<int>());
Assert.AreNotSame(EmptyClass.Empty<string>(), EmptyClass.Empty<object>());
}

Bien sûr, cela ne se vérifie pas que le cache n'est pas per-thread, ou quelque chose comme ça… mais il va le faire.

Voyons l'implémentation !

L'implémentation est en fait légèrement plus intéressante que la description ci-après peut suggérer. Si ce n'était pas pour l'aspect mise en cache, nous pourrions mettre en œuvre comme suit :

// Ne met pas en cache les séquences vides
public static IEnumerable<TResult> Empty<TResult>()
{
yield break;
}

… mais nous voulons obéir à (un peu vague) l'aspect de mise en cache documenté aussi. Ce n'est pas vraiment difficile, à la fin. Il y a un fait très pratique que nous pouvons utiliser : les tableaux vides sont immuables. Les tableaux ont toujours une taille fixe, mais normalement il n'y a aucun moyen de faire un tableau en lecture seule… vous pouvez toujours changer la valeur de tout élément. Mais un tableau vide n'a pas tous les éléments, donc il n'y a rien à changer. Donc, nous pouvons réutiliser le même tableau, encore et encore, le renvoyant directement à l'appelant… mais seulement si nous avons un tableau vide du type de droit.

À ce stade, vous pouvez vous attendre à trouver un Dictionary<Type, Array> ou quelque chose de similaire… mais il y a un autre truc utile dont nous pouvons tirer profit. Si vous avez besoin d'un cache par type et le type est spécifique comme un argument de type, vous pouvez utiliser des variables statiques dans une classe générique, parce que chaque type construit aura un ensemble distinct de variables statiques.

Malheureusement, Empty est une méthode générique plutôt qu'une méthode non générique dans un type générique… donc nous avons à créer un type générique distinct pour agir en tant que notre cache pour le tableau vide. C'est facile à faire cependant, et le CLR se charge d'initialiser le type d'une manière thread-safe, aussi. Donc, notre implémentation finale ressemble à ceci :

public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyHolder<TResult>.Array;
}

private static class EmptyHolder<T>
{
internal static readonly T[] Array = new T[0];
}

Cela obéit à toute la mise en cache dont nous avons besoin, et est très simple en termes de lignes de code… mais cela signifie que vous devez comprendre comment les génériques fonctionnent raisonnablement bien dans .NET. À certains égards, c'est le contraire de la situation dans le post précédent — il s'agit d'une implémentation sournoise au lieu de la plus lente, mais sans doute plus simple sur un dictionnaire-base. Dans ce cas, je suis heureux avec le compromis, car une fois que vous comprenez comment les types génériques et les variables statiques fonctionnent, ce code est simple. C'est un cas où la simplicité est dans l'œil du spectateur.

Conclusion

Donc, c'est Empty. Le prochain opérateur — Repeat — est susceptible d'être encore plus simple, mais il va falloir une autre implémentation de division…

Addenda

En raison d'une révolte mineure de plus de retourner un tableau (qui je pense toujours que c'est très bien), voici une implémentation alternative :

public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyEnumerable<TResult>.Instance;
}

#if AVOID_RETURNING_ARRAYS
private class EmptyEnumerable<T> : IEnumerable<T>, IEnumerator<T>
{
internal static IEnumerable<T> Instance = new EmptyEnumerable<T>();

// Prevent construction elsewhere
private EmptyEnumerable()
{
}

public IEnumerator<T> GetEnumerator()
{
return this;
}

IEnumerator IEnumerable.GetEnumerator()
{
return this;
}

public T Current
{
get { throw new InvalidOperationException(); }
}

object IEnumerator.Current
{
get { throw new InvalidOperationException(); }
}

public void Dispose()
{
// No-op
}

public bool MoveNext()
{
return false; // There's never a next entry
}

public void Reset()
{
// No-op
}
}

#else
private static class EmptyEnumerable<T>
{
internal static readonly T[] Instance = new T[0];
}
#endif

Espérons que maintenant tout le monde pourra construire une version avec laquelle il est heureux :)

Remerciements

Je tiens ici à remercier Jon Skeet de m'avoir autorisé à traduire son article Reimplementing LINQ to Objects: Part 5 - Empty.

Je remercie Tomlev pour sa relecture technique et ses propositions.

Je remercie également Claude Leloup pour sa relecture orthographique et ses propositions.