Réimplémentation de LINQ to Objects : Partie 12 - « DefaultIfEmpty »

Jon Skeet

Traducteur

Rémy Mainil

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

Partie précédente : Réimplémentation de LINQ to Objects : Partie 11 - « First », « Single », « Last » et leur variante « OrDefault »

Partie suivante : Réimplémentation de LINQ to Objects : Partie 13 - « Aggregate »

Commentez l'article : Commentez

- Titre : Réimplémentation de LINQ to Objects : Partie 12 - « DefaultIfEmpty »

- Auteur : Jon Skeet

- Traducteur : Rémy Mainil

- Parution : 14 janvier 2014

- Mise à jour : 14 janvier 2014

- 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

- I. Introduction

- II. Qu'est-ce ?

- III. Qu'allons-nous tester ?

- IV. Voyons l'implémentation !

- V. Conclusion

- VI. Remerciements

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.