Edición 02-Feb-2010:
Tal como lo veo, hay por lo menos dos maneras de interpretar esta pregunta.
¿Por qué el método Select<T, TResult>
extensión, cuando llama en una instancia de una clase que implementa ICollection<T>
, no devolver un objeto que proporciona una propiedad Count
; y ¿por qué el método de extensión Count<T>
no marca esta propiedad para que proporcione rendimiento O (1) cuando los dos métodos están encadenados?
Esta versión de la pregunta no hace suposiciones falsas acerca de cómo funcionan las extensiones de LINQ, y es una pregunta válida desde una llamada a ICollection<T>.Select.Count
será, después de todo, siempre devuelven el mismo valor que ICollection<T>.Count
. Así es como Mehrdad interpretó la pregunta, a la que ha proporcionado una respuesta completa.
Pero I lea la pregunta como preguntando ...
Si el método Count<T>
extensión proporciona un rendimiento O (1) para un objeto de una clase aplicación de ICollection<T>
, ¿por qué tampoco proporciona O (n) rendimiento para el valor de retorno de la extensión Select<T, TResult>
¿método?
En esta versión de la pregunta, existe una suposición errónea: que los métodos de extensión Linq trabajan juntos por unión de pequeñas colecciones uno tras otro (en memoria) y exponiéndolos a través de la interfaz de IEnumerable<T>
.
Si esto fuera cómo funcionaban las extensiones de LINQ, el método Select
podría ser algo como esto:
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
List<TResult> results = new List<TResult>();
foreach (T input in source)
results.Add(selector(input));
return results;
}
Por otra parte, si esto fuera la implementación de Select
, creo que iba a encontrar la mayoría del código que utiliza este método se comportaría de la misma manera. Pero sería un desperdicio, y de hecho causaría excepciones en ciertos casos como el que describí en mi respuesta original.
En realidad, creo que la implementación del método Select
es mucho más cercano a algo como esto:
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
foreach (T input in source)
yield return selector(input);
yield break;
}
Se trata de proporcionar la evaluación perezosa, y explica por qué una propiedad Count
no es accesible en O (1) tiempo para el método Count
.
En otras palabras, mientras que Mehrdad respondió a la pregunta de por qué Select
no fue diseñado forma diferente para que Select.Count
se comportan de manera diferente, he ofrecido mi mejor respuesta a la pregunta de por qué Select.Count
comporta como lo hace.
respuesta original: efectos secundarios
método no es la respuesta.
Según la respuesta de Mehrdad:
Realmente no importa que el resultado de Select se evalúa con pereza.
No compro esto. Déjame explicar por qué.
Para empezar, tenga en cuenta los siguientes dos métodos muy similares:
public static IEnumerable<double> GetRandomsAsEnumerable(int N) {
Random r = new Random();
for (int i = 0; i < N; ++i)
yield return r.NextDouble();
yield break;
}
public static double[] GetRandomsAsArray(int N) {
Random r = new Random();
double[] values = new double[N];
for (int i = 0; i < N; ++i)
values[i] = r.NextDouble();
return values;
}
bien, ¿qué hacen estos métodos? Cada uno devuelve tantos dobles aleatorios como desee el usuario (hasta int.MaxValue
). ¿Importa si alguno de los métodos es evaluado o no?Para responder a esta pregunta, vamos a echar un vistazo al código siguiente:
public static double Invert(double value) {
return 1.0/value;
}
public static void Test() {
int a = GetRandomsAsEnumerable(int.MaxValue).Select(Invert).Count();
int b = GetRandomsAsArray(int.MaxValue).Select(Invert).Count();
}
se puede adivinar lo que sucederá con estos dos llamadas a métodos? Deja que te ahorraré la molestia de copiar el código y probar por ti mismo:
La primera variables, a
, será (después de una cantidad potencialmente significativo de tiempo) se inicializa a int.MaxValue
(actualmente 2147483647). El segundo uno, b
, muy probablemente será interrumpido por un OutOfMemoryException
.
Dado que Select
y los otros métodos de extensión de Linq se evalúan con holgazanería, le permiten hacer cosas que simplemente no podría hacer de otra manera. Lo anterior es un ejemplo bastante trivial. Pero mi punto principal es disputar la afirmación de que la evaluación perezosa no es importante. La afirmación de Mehrdad de que una propiedad Count
"es realmente conocida desde el principio y podría optimizarse" en realidad plantea la pregunta. El problema puede parecer sencillo para el método Select
, pero Select
no es realmente especial; devuelve un IEnumerable<T>
al igual que el resto de los métodos de extensión Linq, y para que estos métodos "conozcan" el Count
de sus valores de retorno requeriría que las colecciones completas se almacenaran en caché y, por lo tanto, prohibiría la evaluación diferida.
La evaluación lenta es la respuesta.
Por esta razón, tengo que estar de acuerdo con uno de los respondedores originales (cuya respuesta ahora parece haber desaparecido) que evaluación perezosa realmente es la respuesta aquí. La idea de que los efectos secundarios del método deben tenerse en cuenta es realmente secundaria, ya que esto ya está garantizado como un subproducto de la evaluación perezosa de todos modos.
Posdata: He hecho declaraciones muy enérgicas y he enfatizado mis puntos principalmente porque quería dejar en claro cuál es mi argumento, no por falta de respeto a ninguna otra respuesta, incluida la de Mehrdad, que creo que es perspicaz, pero pierde la marca.
¿Qué hay de la propiedad indexadora de ICollection que tiene un efecto secundario? ¿No es preocupante que la optimización implementada actualmente en el método Count evite la llamada a la propiedad del indexador de la colección? – shojtsy
@shojtsy: el indexador es irrelevante y nunca es utilizado por ninguno de los métodos LINQ, pero su preocupación es válida ya que la propiedad 'Count' y el método' GetEnumerator' también pueden tener efectos secundarios. Es por eso que la documentación del método 'Count()' distingue explícita y claramente 'ICollection' y dice que usa la propiedad' Count' si el argumento implementa esa interfaz en lugar de enumeración. Si esto no estaba claro en el documento, hubiera esperado 'Count()' ** no ** usar 'ICollection .Count' tampoco. –
Su respuesta puede abordar este * escenario * exacto ('Seleccionar' en una' ICollection') directamente, pero creo que es engañoso porque podría interpretarse erróneamente para sugerir que esta posible optimización fue alguna vez considerada seriamente, y solo descartada para permitir efectos secundarios. En mi respuesta, estoy tratando de: primero: explicar por qué las extensiones * de Linq * en general * usan evaluación diferida; y segundo: señale que 'Seleccionar 'es * no * especial, y funciona igual que cualquier otra extensión de Linq. –