LINQ: Deferred Execution (Ejecución diferida): Cosas a tener en cuenta
Pues como dice el título, la ejecución diferida es una característica de algunos operadores de LINQ, qué sólo cuando se itera sobre los elementos del enumerable, se evalua la consulta (cuando se llama al método MoveNext) y no cuando se construye.
No todos los operadores de LINQ causan esta ejecución diferida, hay excepciones:
- Operadores que retornan un sólo elemento o un valor escalar, como First, Last, Count…
- Operadores de conversión como ToArray, ToList…
Vamos a ver todo esto con un ejemplo:
///Deferred Execution
List<string> names = new List<string>();
names.Add("Juan");
names.Add("Pepe");
names.Add("Carlos");
names.Add("Jacinto");
var greaterthanfour = from n in names
where n.Length > 4
select n;
names[2] = "Ciro";
foreach (string name in greaterthanfour)
Console.WriteLine(name);
Console.Read();
Sí observamos la consulta, en ella se obtienen los nombres cuya longitud es mayor de 4, en tal caso la respuesta debería ser:
Carlos y Jacinto, no?
Pues no, porque como hemos comentado, la consulta no se evalua hasta que se itera sobre ella (instrucción foreach), y por tanto Carlos ha sido sustituido por Ciro, con lo cual el resultado es:
Otra problema que nos podemos encontrar con la ejecución diferida es la Reevalución, es decir, la reevalución se produce cuando reenumeramos. Dada la siguiente consulta:
///Deferred Execution -> Reevaluation
List<string> names = new List<string>();
names.Add("Juan");
names.Add("Pepe");
names.Add("Carlos");
names.Add("Jacinto");
var greaterthanfour = from n in names
where n.Length > 4
select n;
Console.WriteLine("foreach 1");
foreach (string name in greaterthanfour)
Console.WriteLine(name);
names.Clear();
Console.WriteLine("foreach 2");
foreach (string name in greaterthanfour)
Console.WriteLine(name);
Console.Read();
En el primer foreach, obtendremos como resultado:
Y en el segundo, no se muestra nada :S, porque hemos limpiado la lista y estamos volviendo a reenumerar, lo que conlleva una reevaluación:
Para evitar la reevaluación de expresiones (Dado que esta reevalución implica un costo adicional, sobre todo sí estamos ejecutando consultas sobre una base de datos), podemos utilizar métodos como ToArray, ToList… que causan la ejecución inmediata de la consulta y posteriormente trabajar sobre esa copia o caché:
///Deferred Execution -> Reevaluation -> Cache Names
List<string> namesCache = (from n in names
where n.Length > 4
select n).ToList();
Console.WriteLine("foreach 1");
foreach (string name in namesCache)
Console.WriteLine(name);
names.Clear();
Console.WriteLine("foreach 2");
foreach (string name in namesCache)
Console.WriteLine(name);
Console.Read();
Resultado:
Y por último, otro problema que nos encontramos con la ejecución diferida, es cuando dentro una consulta LINQ usamos variables externas o Outer Variables. Veáse el siguiente ejemplo:
///Deferred Execution -> Outer Variables
List<int> salary = new List<int>();
salary.Add(1800);
salary.Add(1500);
int plus = 1000;
IEnumerable<int> totalSalary = salary.Select(s => s + plus);
plus = 500;
foreach (int total in totalSalary)
Console.WriteLine(total);
Console.Read();
El problema en este caso, es que estamos usando una variable externa (plus) en la consulta, que inicilamente se incializa a 1000, con lo que podríamos esperar que el resultado fuera o fuese 2800 y 2500, pero antes de iterar, cambiamos su valor a 500, y es este último el que se utilizará para calcular el salario total:
Así que a partir de ahora, cuidado con la ejecución diferida ;)
Bibliografía: C# 3.0 in a Nutshell
Esto es todo!!!