Lors du profilage de l'une de nos applications, nous avons découvert un mystérieux ralentissement dans un code dans lequel nous appelions Enumerable.Single(source, predicate)
pour une grande collection dont plusieurs éléments correspondaient au prédicat au début de la collection.
L’enquête a révélé que l’implémentation de Enumerable.Single()
est la suivante:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
// Note how this always iterates through ALL the elements:
foreach (TSource element in source) {
if (predicate(element)) {
result = element;
checked { count++; }
}
}
switch (count) {
case 0: throw Error.NoMatch();
case 1: return result;
}
throw Error.MoreThanOneMatch();
}
Cette implémentation itérera à travers chaque élément de la séquence, même si plusieurs éléments ont déjà correspondu au prédicat.
L'implémentation suivante semble donner les mêmes résultats:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
foreach (TSource element in source) {
if (predicate(element)) {
if (count == 1) // Exit loop immediately if more than one match found.
throw Error.MoreThanOneMatch();
result = element;
count++; // "checked" is no longer needed.
}
}
if (count == 0)
throw Error.NoMatch();
return result;
}
Est-ce que quelqu'un sait pourquoi l'implémentation réelle n'utilise pas cette optimisation évidente? Y a-t-il quelque chose qui me manque? (Je ne peux pas imaginer qu'une telle optimisation soit négligée et qu'il doit donc y avoir une raison concrète.)
(Remarque: je me rends compte que cette question peut attirer des réponses qui sont des opinions; j'espère des réponses qui fournissent des raisons concrètes pour itérer tous les éléments. Si la réponse est réellement "parce que les concepteurs n'ont pas pensé qu'une telle optimisation était nécessaire", alors cette question est sans réponse et je suppose que je devrais juste le supprimer ...)
Pour comparer, regardez l'implémentation de Single()
qui ne prend pas de prédicat:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
IList<TSource> list = source as IList<TSource>;
if (list != null) {
switch (list.Count) {
case 0: throw Error.NoElements();
case 1: return list[0];
}
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (!e.MoveNext()) throw Error.NoElements();
TSource result = e.Current;
if (!e.MoveNext()) return result;
}
}
throw Error.MoreThanOneElement();
}
Dans ce cas, ils ont tenté d'ajouter une optimisation pour IList
.
Vous n'avez pas semblé être le seul à penser cela. Le implémentation .NET Core a une version optimisée:
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
Donc, pour répondre à votre question, il ne semble pas y avoir de "bonne" raison, si ce n’est qu’un développeur qui ne songe pas à optimiser ce cas d’utilisation.
L'optimisation a été appliqué dans .NET Core
Le code est maintenant:
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (predicate == null)
{
throw Error.ArgumentNull(nameof(predicate));
}
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
throw Error.NoMatch();
}
Dans la mesure du possible, le code vérifie même si la cible est un IList<T>
pour éviter les itérations:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (source is IList<TSource> list)
{
switch (list.Count)
{
case 0:
throw Error.NoElements();
case 1:
return list[0];
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
throw Error.NoElements();
}
TSource result = e.Current;
if (!e.MoveNext())
{
return result;
}
}
}
throw Error.MoreThanOneElement();
}
[~ # ~] met à jour [~ # ~]
Vérifier la sortie git blame montre que l'optimisation des itérations a été appliquée en 2016!
Le IList<>
_ optimisation a été ajoutée il y a 1 an, probablement dans le cadre des optimisations Core 2.1
Comme les autres réponses l'ont souligné, l'optimisation a été appliquée, mais je voudrais simplement émettre l'hypothèse qu'ils l'avaient fait de cette façon, en pensant à l'origine qu'ils n'avaient aucun moyen de garantir que la fonction de prédicat n'avait pas de côté. effets.
Je ne suis pas sûr qu'il y aurait vraiment un cas où un tel comportement serait utilisé/utile, mais il faut en tenir compte.