I. Introduction▲
Après le volume de code nécessaire pour les opérateurs First, Last, etc., l'opérateur DefaultIfEmpty apporte un peu de repos.
II. Qu'est-ce ?▲
Même ce simple opérateur dispose de deux surcharges :
public
static
IEnumerable<
TSource>
DefaultIfEmpty<
TSource>(
this
IEnumerable<
TSource>
source)
public
static
IEnumerable<
TSource>
DefaultIfEmpty<
TSource>(
this
IEnumerable<
TSource>
source,
TSource defaultValue)
Le comportement est très simple à décrire :
- si la séquence d'entrée est vide, la séquence résultante est composée d'un seul élément, la valeur par défaut. Il s'agit de default(TSource) pour la surcharge sans paramètre supplémentaire, ou de la valeur spécifiée dans le cas contraire ;
- si la séquence d'entrée n'est pas vide, la séquence résultante est identique ;
- l'argument source ne peut pas être null et la validation a lieu immédiatement ;
- la séquence résultante elle-même utilise l'exécution différée. La séquence d'entrée elle-même n'est pas parcourue tant que la séquence résultante n'est pas parcourue ;
- la séquence d'entrée est streamée : chaque valeur lue est produite directement, il n'y a pas de buffering.
Rien de plus simple.
III. Qu'allons-nous tester ?▲
Bien qu'il soit relativement tard dans la journée, j'ai décidé de tester la validité des arguments et c'était une bonne chose, ma première tentative de séparation de l'implémentation en une méthode de validation des arguments et une méthode d'itération pour le vrai travail ayant échoué. Cela montre simplement comment il est facile de cafouiller.
A part ça, je n'ai trouvé que quatre cas qui valent la peine d'être testés :
- pas de valeur par défaut précisée, séquence d'entrée vide ;
- une valeur par défaut, séquence d'entrée vide ;
- pas de valeur par défaut précisée, séquence d'entrée non-vide ;
- une valeur par défaut, séquence d'entrée non-vide.
Donc j'ai des tests pour tous ces cas, et c'est tout ! Je n'ai rien pour tester le streaming ou l'évaluation différée.
IV. Voyons l'implémentation !▲
Mise à part ma réticence jusqu'ici à implémenter un opérateur en fonction d'un autre, cela me semble un cas vraiment évident. Je me suis dit que dans ce cas, ça avait du sens. J'ai même appliqué cela à la validation des arguments. Voici l'implémentation dans toute sa splendeur :
public
static
IEnumerable<
TSource>
DefaultIfEmpty<
TSource>(
this
IEnumerable<
TSource>
source)
{
// This will perform an appropriate test for source being null first.
return
source.
DefaultIfEmpty
(
default
(
TSource));
}
public
static
IEnumerable<
TSource>
DefaultIfEmpty<
TSource>(
this
IEnumerable<
TSource>
source,
TSource defaultValue)
{
if
(
source ==
null
)
{
throw
new
ArgumentNullException
(
"source"
);
}
return
DefaultIfEmptyImpl
(
source,
defaultValue);
}
private
static
IEnumerable<
TSource>
DefaultIfEmptyImpl<
TSource>(
IEnumerable<
TSource>
source,
TSource defaultValue)
{
bool
foundAny =
false
;
foreach
(
TSource item in
source)
{
yield
return
item;
foundAny =
true
;
}
if
(!
foundAny)
{
yield
return
defaultValue;
}
}
Évidemment, maintenant que j'ai dit combien c'était simple, quelqu'un va trouver un bogue…
Mise à part l'utilisation de default(TSource) pour appeler la surcharge plus complexe depuis la plus simple, le seul aspect intéressant est la méthode du bas. Assigner « true » à « foundAny » à chaque itération m'irrite légèrement mais l'alternative est assez déplaisante :
private
static
IEnumerable<
TSource>
DefaultIfEmptyImpl<
TSource>(
IEnumerable<
TSource>
source,
TSource defaultValue)
{
using
(
IEnumerator<
TSource>
iterator =
source.
GetEnumerator
(
))
{
if
(!
iterator.
MoveNext
(
))
{
yield
return
defaultValue;
yield
break
;
// Like a "return"
}
yield
return
iterator.
Current;
while
(
iterator.
MoveNext
(
))
{
yield
return
iterator.
Current;
}
}
}
Cela pourrait être légèrement plus efficace mais cela semble plus maladroit. Nous pourrions nous passer de l'instruction « yield break » en plaçant le reste de la méthode dans un bloc « else » mais ça ne m'emballe pas non plus. Nous pourrions utiliser une boucle do-while en remplacement de la boucle while, cela retirerait au moins la répétition de l'instruction yield return iterator.Current mais je ne suis pas vraiment un fan des boucles do-while. Je les utilise si peu que cela me demande plus d'effort mental pour les relire que je ne voudrais.
Si un lecteur a des suggestions qui sont significativement meilleures que les implémentations ci-dessus, je serais intéressé d'en prendre connaissance. Cela semble un peu inélégant pour l'instant mais c'est loin d'être un désastre de lisibilité. Cela manque juste un peu de soin.
V. Conclusion▲
Mis à part le léger ennui provoqué par le manque d'élégance, rien de vraiment intéressant ici. Cependant, nous pouvons maintenant implémenter FirstOrDefault, LastOrDefault et SingleOrDefault en utilisant DefaultIfEmpty. Voici, par exemple, une implémentation de FirstOrDefault :
public
static
TSource FirstOrDefault<
TSource>(
this
IEnumerable<
TSource>
source)
{
return
source.
DefaultIfEmpty
(
).
First
(
);
}
public
static
TSource FirstOrDefault<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
{
// Can't just use source.DefaultIfEmpty().First(predicate)
return
source.
Where
(
predicate).
DefaultIfEmpty
(
).
First
(
);
}
Notez le commentaire dans la version avec prédicat. L'utilisation de la valeur par défaut doit être la toute dernière étape après l'application du prédicat sinon, si nous passons une séquence vide et un prédicat qui ne valide pas default(TSource), nous aurons une exception plutôt que la valeur par défaut. Les deux autres opérateurs OrDefault peuvent être implémentés de la même manière, évidemment. (Je ne l'ai pas encore fait, mais le code ci-dessus est dans le contrôleur de code source.)
Je ne suis actuellement pas encore certain de ce que je vais implémenter prochainement. Je verrai si je suis inspiré par une quelconque méthode dans la matinée.
VI. Remerciements▲
Merci à Jon Skeet de m'avoir autorisé à traduire son article Reimplementing LINQ to Objects: Part 12 - DefaultIfEmpty
Merci à Thomas Levesque (tomlev) pour sa relecture technique toujours bienvenue.
Merci à Philippe DUVAL (Phanloga) pour la relecture orthographique de ce document.