c# linq .net-4.0

c# - ¿Por qué Enumerable.Single() itera todos los elementos, incluso cuando ya se ha encontrado más de un elemento?



linq .net-4.0 (3)

Al perfilar una de nuestras aplicaciones, descubrimos una desaceleración misteriosa en algún código donde llamábamos Enumerable.Single(source, predicate) para una colección grande que tenía más de un elemento que coincidía con el predicado cerca del inicio de la colección.

La investigación reveló que la implementación de Enumerable.Single() es la siguiente:

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(); }

Esa implementación se repetirá en todos los elementos de la secuencia, incluso si más de un elemento ya ha coincidido con el predicado.

La siguiente implementación parecería dar los mismos resultados:

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; }

¿Alguien sabe por qué la implementación real no usa esta obvia optimización? ¿Se me escapa algo? (No puedo imaginar que una optimización tan obvia sea pasada por alto, y por lo tanto, debe haber alguna razón concreta para ello).

(Nota: me doy cuenta de que esta pregunta puede atraer respuestas que son opiniones; espero respuestas que proporcionen razones concretas para iterar todos los elementos. Si la respuesta es en realidad "porque los diseñadores no pensaron que tal optimización fuera necesaria", entonces esta pregunta no tiene respuesta y supongo que debería borrarla ...)

Para comparar, mire la implementación de Single() que no tiene un predicado:

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(); }

En este caso, se han esforzado por agregar una optimización para IList .


Como han señalado las otras respuestas, se ha aplicado la optimización, pero solo me gustaría plantear la hipótesis de que lo habían hecho de esa manera, pensando originalmente que no tienen forma de garantizar que la función de predicado no tenga sentido. efectos

No estoy seguro de que realmente haya un caso en el que ese comportamiento sea usado / útil, pero es una consideración a tener en cuenta.


La optimización se aplicó en .NET Core

El código ahora es:

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(); }

Siempre que sea posible, el código incluso comprueba si el objetivo es un IList<T> para evitar la iteración:

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(); }

ACTUALIZAR

La comprobación de la salida de Git Culge muestra que la optimización de iteración se aplicó en 2016.

La optimización IList<> se agregó hace 1 año, probablemente como parte de las optimizaciones Core 2.1


No parecías ser el único que pensaba eso. La implementación de .NET Core tiene una versión optimizada:

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; } } }

Entonces, para responder a su pregunta: no parece haber una "buena" razón, aparte de un simple desarrollador que no está pensando en optimizar este caso de uso.