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.