LINQ y IEnumerable<T>
es tirón basado. Esto significa que los predicados y acciones que son parte de la declaración LINQ en general no se ejecutan hasta que se extraen los valores. Además, los predicados y las acciones se ejecutarán cada vez que se extraen los valores (por ejemplo, no hay almacenamiento en caché secreto).
Tirando de un IEnumerable<T>
se realiza mediante la declaración foreach
que en realidad es el azúcar sintáctica para conseguir un empadronador llamando IEnumerable<T>.GetEnumerator()
y repetidamente llamar IEnumerator<T>.MoveNext()
para tirar de los valores.
operadores LINQ como ToList()
, ToArray()
, ToDictionary()
y ToLookup()
envuelve una declaración foreach
por lo que estos métodos harán un tirón. Lo mismo puede decirse de operadores como Aggregate()
, Count()
y First()
. Estos métodos tienen en común que producen un solo resultado que debe crearse ejecutando una instrucción foreach
.
Muchos operadores LINQ producen una nueva secuencia IEnumerable<T>
. Cuando se extrae un elemento de la secuencia resultante, el operador extrae uno o más elementos de la secuencia de origen. El operador Select()
es el ejemplo más obvio, pero otros ejemplos son SelectMany()
, Where()
, Concat()
, Union()
, Distinct()
, Skip()
y Take()
. Estos operadores no hacen ningún almacenamiento en caché. Cuando el elemento N'th se extrae de un Select()
, extrae el elemento N-ésimo de la secuencia de origen, aplica la proyección con la acción suministrada y la devuelve. Nada secreto pasando aquí.
Otros operadores LINQ también producen nuevas secuencias IEnumerable<T>
, pero se implementan al extraer realmente toda la secuencia de origen, hacer su trabajo y luego producir una nueva secuencia. Estos métodos incluyen Reverse()
, OrderBy()
y GroupBy()
. Sin embargo, la extracción realizada por el operador solo se lleva a cabo cuando se tira del operador, lo que significa que aún necesita un bucle foreach
"al final" de la instrucción LINQ antes de ejecutar cualquier cosa. Podría argumentar que estos operadores usan una memoria caché porque extraen inmediatamente toda la secuencia fuente. Sin embargo, este caché se genera cada vez que se itera el operador, por lo que es realmente un detalle de implementación y no algo que detectará mágicamente que está aplicando la misma operación OrderBy()
varias veces en la misma secuencia.
En su ejemplo la ToList()
hará un tirón. La acción en el exterior Select
se ejecutará 100 veces. Cada vez que se ejecuta esta acción, el Aggregate()
hará otra extracción que analizará los atributos XML. En total, su código llamará al Int32.Parse()
200 veces.
Puede mejorar esta tirando de los atributos de una vez en lugar de en cada iteración:
var X = XElement.Parse (@"
<ROOT>
<MUL v='2' />
<MUL v='3' />
</ROOT>
")
.Elements()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList();
Enumerable.Range (1, 100)
.Select (s => x.Aggregate (s, (t, u) => t * u))
.ToList()
.ForEach (s => Console.WriteLine (s));
Ahora Int32.Parse()
sólo se le llama 2 veces. Sin embargo, el costo es que una lista de valores de atributo debe asignarse, almacenarse y eventualmente recolectarse como basura. (No es un gran preocupación cuando la lista contiene dos elementos.)
Tenga en cuenta que si se olvida la primera ToList()
que tira de los atributos del código seguirá funcionando pero con exactamente las mismas características de rendimiento como el código original. No se utiliza espacio para almacenar los atributos, pero se analizan en cada iteración.
"cómo podría averiguar algo como esto yo mismo" - la mejor oportunidad es estudiar el IL generado a partir de este código. – Andrey
Puede establecer un punto de interrupción del depurador en el método Parse() y ver la frecuencia con la que golpea. –