I. Introduction▲
Un autre jour, un autre billet. Je tiens à souligner que ce rythme de publication est susceptible d'être de courte durée. Quoique si je prends l'habitude d'écrire un billet sur le trajet du matin quand je reprendrai le travail après les vacances de Noël, je pourrais garder le rythme jusqu'à ce que nous ayons terminé.
Quoi qu'il en soit, aujourd'hui nous avons deux opérateurs à traiter : « Any » et « All ».
II. Que sont-ils ?▲
« Any » dispose de deux surcharges, « All » d'une seule :
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source)
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
public
static
bool
All<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
Leurs noms parlent d'eux-mêmes :
- « Any » sans prédicat renvoie vrai s'il existe au moins un élément dans la séquence d'entrée ;
- « Any » avec un prédicat renvoie vrai si au moins un élément de la séquence d'entrée valide le prédicat ;
- « All » renvoie vrai si tous les éléments de la séquence d'entrée valident le prédicat donné.
Chacun des opérateurs fonctionne selon le principe de l'exécution immédiate. Fondamentalement, ils ne se terminent que lorsqu'ils ont la réponse.
À noter que « All » doit parcourir toute la séquence pour retourner vrai, mais peut renvoyer faux aussitôt qu'il trouve un élément qui ne valide pas le prédicat. « Any » peut renvoyer vrai dès qu'il trouve un élément correspondant mais doit parcourir toute la séquence pour retourner faux. Cela donne lieu à une astuce très simple pour améliorer les performances de LINQ : ce n'est quasiment jamais une bonne idée d'utiliser une requête telle que celle ci-dessous.
// Ne faites pas ça !
if
(
query.
Count
(
) !=
0
)
Elle doit en effet parcourir tous les résultats de la requête alors que ce qui importe est de savoir si oui ou non il y a au moins un résultat. Il vaut donc mieux utiliser « Any » :
// Préférez ceci :
if
(
query.
Any
(
))
Si cela fait partie d'une plus grande requête LINQ to SQL, cela ne fera peut-être pas de différence. Néanmoins dans LINQ to Objects cela peut certainement être un avantage énorme.
Soit. Allons tester nos trois méthodes.
III. Qu'allons-nous tester ?▲
D'humeur vertueuse ce soir, j'ai même testé à nouveau la validation des arguments. Dans ce cas, cependant, le mérite n'est pas très grand comme nous utilisons l'exécution immédiate.
Ceci dit, j'ai testé différents scénarios :
- une séquence vide retournera faux avec Any mais vrai avec All (peu importe le prédicat utilisé pour All, aucun élément n'échouera à sa validation) ;
- une séquence avec au minimum un élément retournera vrai pour la version de Any sans prédicat ;
- si aucun élément ne valide le prédicat, tant Any que All retournera faux ;
- si certains éléments valident le prédicat, Any retournera vrai tandis que All retournera faux ;
- si tous les éléments valident le prédicat, All retournera vrai.
Tous ces tests sont évidents, je ne fournirai donc pas le code. Il reste un test intéressant : nous allons prouver que Any se termine dès qu'il a un résultat en lui fournissant une requête qui lève une exception si elle est parcourue dans son intégralité. Le moyen le plus simple de procéder revient à démarrer avec une séquence d'entiers, y compris 0, et ensuite d'utiliser Select pour créer une projection qui divise une valeur constante par chacun des éléments. Dans ce test, j'ai fourni une valeur qui vérifiera le prédicat avant la valeur qui provoque la levée de l'exception :
[Test]
public
void
SequenceIsNotEvaluatedAfterFirstMatch
(
)
{
int
[]
src =
{
10
,
2
,
0
,
3
};
var
query =
src.
Select
(
x =>
10
/
x);
// This will finish at the second element (x = 2, so 10/x = 5)
// It won't evaluate 10/0, which would throw an exception
Assert.
IsTrue
(
query.
Any
(
y =>
y >
2
));
}
Il y a un test équivalent pour « All » où un élément non-concordant est présent avant l'élément qui lève l'exception.
Maintenant que tous les tests sont écrits, continuons avec la partie intéressante.
IV. Voyons l'implémentation !▲
La première chose à noter est que chacun peut être implémenté soit sous la forme « Any avec un prédicat », soit « All ». Par exemple, si on utilise All, nous pouvons implémenter Any comme ceci :
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source)
{
return
source.
Any
(
x =>
true
);
}
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
{
if
(
predicate ==
null
)
{
throw
new
ArgumentNullException
(
"predicate"
);
}
return
!
source.
All
(
x =>
!
predicate
(
x));
}
C'est plus simple d'implémenter Any dans sa version sans prédicat en utilisant la version avec prédicat. Utiliser un prédicat qui retournera vrai pour n'importe quel élément revient à dire que Any retournera vrai pour n'importe quel élément, soit exactement ce qu'on souhaite.
Comprendre les négations dans l'appel à All nécessite une petite minute. Elles sont simplement la Loi de De Morgan sous la forme LINQ. Nous inversons effectivement les prédicats de façon à déterminer si tous les éléments ne vérifient pas le prédicat original, pour retourner ensuite l'inverse. L'inversion permet également de terminer au plus tôt dans toutes les situations appropriées.
Bien que nous puissions faire comme ci-dessus, j'ai préféré une implémentation plus conventionnelle en implémentant les différentes méthodes séparément :
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source)
{
if
(
source ==
null
)
{
throw
new
ArgumentNullException
(
"source"
);
}
using
(
IEnumerator<
TSource>
iterator =
source.
GetEnumerator
(
))
{
return
iterator.
MoveNext
(
);
}
}
public
static
bool
Any<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
{
if
(
source ==
null
)
{
throw
new
ArgumentNullException
(
"source"
);
}
if
(
predicate ==
null
)
{
throw
new
ArgumentNullException
(
"predicate"
);
}
foreach
(
TSource item in
source)
{
if
(
predicate
(
item))
{
return
true
;
}
}
return
false
;
}
public
static
bool
All<
TSource>(
this
IEnumerable<
TSource>
source,
Func<
TSource,
bool
>
predicate)
{
if
(
source ==
null
)
{
throw
new
ArgumentNullException
(
"source"
);
}
if
(
predicate ==
null
)
{
throw
new
ArgumentNullException
(
"predicate"
);
}
foreach
(
TSource item in
source)
{
if
(!
predicate
(
item))
{
return
false
;
}
}
return
true
;
}
En dehors de toute autre chose, cela met en évidence le moment ou la sortie prématurée entre en jeu et cela signifie également que toute pile d'appel générée en cas d'erreur sera plus facile à comprendre. De plus, du point de vue d'un développeur client, cela semblerait étrange lors d'un appel à Any de voir apparaître All dans la pile d'appel et vice-versa.
Un élément intéressant est que je n'utilise finalement pas de boucle foreach dans l'implémentation de Any bien que j'aurais pu, évidemment. À la place, je me contente de récupérer l'itérateur et je retourne la valeur du premier appel à MoveNext qui indique s'il y a au moins un élément. J'aime le fait qu'à la lecture de cette méthode cela saute aux yeux (au moins pour moi) que l'on ne se préoccupe pas de la valeur du premier élément parce qu'après tout, on ne la demande jamais.
V. Conclusion▲
Probablement que la leçon la plus importante ici consiste à utiliser Any (sans prédicat) plutôt que Count quand on en a la possibilité. Le reste était vraiment simple bien qu'il soit toujours amusant de voir un opérateur implémenté via un autre opérateur.
Hé bien, quelle est la suite ? Peut-être Single/SingleOrDefault/First/FirstOrDefault/Last/LastOrDefault. Je pourrais aussi bien les traiter tous en même temps, d'une part parce qu'ils sont très similaires, d'autre part pour mettre en exergue les différences qui sont présentes.
VI. Remerciements▲
Merci à Jon Skeet de m'avoir autorisé à traduire son article Reimplementing LINQ to Objetcs : Part 10 - Any and All
Merci à Thomas Levesque (tomlev) pour sa relecture technique toujours bienvenue.
Merci à f-leb pour la relecture orthographique de ce document.