LINQ & Deferred Execution
Execucao adiada, IEnumerable vs IQueryable, closures e armadilhas classicas
Regra principal: operadores LINQ como
Where, Select, OrderBy sao lazy β nada executa ate voce enumerar (foreach, ToList, Count...). Isso e a deferred execution. Pipeline de execucao LINQ
O que e Deferred Execution?
Quando voce chama Where() ou Select(), nenhum dado e processado. Voce esta apenas definindo o pipeline. A execucao so acontece quando alguem enumera o resultado β via foreach, ToList(), Count(), etc.
// Deferred β nada executa ao definir o pipeline
var nums = new List<int> { 1, 2, 3, 4 };
var query = nums.Where(n => n > 2); // deferred; nothing executed yet
nums.Add(5); // modifica APOS criar a query
Console.WriteLine(string.Join(",", query)); // prints 3,4,5
// O 5 aparece porque a query so executa AGORA β na enumeracao
Materializacao com ToList()
Materializadores como ToList(), ToArray(), ToHashSet() executam o pipeline imediatamente e armazenam o resultado. Mudancas na fonte apos a materializacao nao afetam o snapshot.
// Materialization β snapshot vs live view
var nums = new List<int> { 1, 2, 3, 4 };
var live = nums.Where(n => n % 2 == 0); // deferred (live view)
var frozen = nums.Where(n => n % 2 == 0).ToList(); // executed now (snapshot)
nums.Add(6);
Console.WriteLine(string.Join(",", live)); // 2,4,6 (ve os novos dados)
Console.WriteLine(string.Join(",", frozen)); // 2,4 (snapshot tirado antes)
Simulador: Deferred vs Immediate
A diferenca esta no codigo: deferred monta um pipeline e so executa no final. Immediate materializa a cada passo.
Codigo Deferred:
var source = new List<int> { 1, 2, 3, 4, 5 };
// Linha 1: NADA executa β retorna IEnumerable<int> (lazy)
var query = source
.Where(n => n > 2) // nao filtra ainda
.Select(n => n * 10); // nao projeta ainda
// "query" e so uma receita β zero processamento ate aqui
// source poderia mudar e o resultado refletiria
// Linha 2: AGORA executa tudo de uma vez
var result = query.ToList(); // [30, 40, 50]
1
Source (List<int>)
Dados na memoria: [1, 2, 3, 4, 5]
aguardando
2
Where(n => n > 2)
Filtra elementos maiores que 2
aguardando
3
Select(n => n * 10)
Projeta multiplicando por 10
aguardando
4
ToList()
Materializa β executa o pipeline agora
aguardando
Perigo: enumeracao multipla
Cada vez que voce enumera um IEnumerable, o pipeline re-executa. Se a fonte e cara (query ao banco, API call), voce paga o custo multiplas vezes.
// Perigo: enumeracao multipla re-executa o pipeline
IEnumerable<int> Expensive()
{
Console.WriteLine("Computing...");
return Enumerable.Range(1, 3).Select(n => { Console.WriteLine($"n={n}"); return n; });
}
var pipeline = Expensive().Where(n => n > 1); // no work yet
Console.WriteLine(pipeline.Sum()); // triggers enumeration #1 β "Computing..." + logs
Console.WriteLine(pipeline.Count()); // triggers enumeration #2 β "Computing..." + logs AGAIN
// Solucao: materializar uma vez
var list = pipeline.ToList(); // executa 1x
Console.WriteLine(list.Sum()); // usa lista em memoria
Console.WriteLine(list.Count()); // sem re-execucao
Qual e o perigo de enumerar multiplas vezes (multiple enumeration)?
Cada enumeracao re-executa todo o pipeline. Se a fonte e uma query de banco, voce faz N queries ao inves de 1. Se a fonte produz side-effects (logging, timers), eles se repetem. A solucao e materializar com
ToList() e usar a lista materializada para Sum(), Count(), etc. Alem disso, se os dados mudarem entre enumeracoes, os resultados serao inconsistentes.Metodos que materializam
Retornam colecao
ToList()ToListAsync()ToArray()ToHashSet()ToDictionary()ToLookup()Retornam elemento unico
First()FirstOrDefault()Single()SingleOrDefault()Last()ElementAt()Retornam escalar
Count()LongCount()Any()All()Sum()Average()Min()Max()Contains()Iteracao implicita
foreach