Já
foi vítima de uma instrução LINQ lenta ?
![]() |
Neste artigo veremos como não abusar da LINQ e fugir de problemas recorrentes. |
A LINQ é um dos
recursos mais elegantes do C# — compacto, expressivo e limpo. Mas, em sistemas
de alto tráfego ou loops críticos, ela pode destruir silenciosamente o
desempenho das suas consultas.
Muitos desenvolvedores amam a LINQ por sua legibilidade, mas
ela tem um custo: alocações de delegates, overhead de enumeradores e pressão
desnecessária na memória.
A boa notícia ? Você não precisa abandoná-la.
Só precisa usá-la com consciência. Vamos ver isso na prática...
1. Não Chame .ToList() Mais do que Você Precisa
Isso é
mais comum do que deveria:
var resultado = ObterUsuarios().Where(u =>
u.EstaAtivo);
if (resultado.Any()) { /* lógica */
}
foreach (var usuario in resultado) { /* código */
}
Se ObterUsuarios() retorna um IEnumerable,
a consulta possui execução adiada. Nesse caso , ela será enumerada duas vezes.
1- Durante o .Any
que percorre a sequência até encontrar o primeiro elemento
2- Durante o foreach que percorre a sequência novamente
Melhor
alternativa:
var
usuariosAtivos = ObterUsuarios().Where(u => u.EstaAtivo).ToList();
if
(usuariosAtivos .Count > 0) { /* lógica */ }
foreach(var usuario in usuariosAtivos ){/* código*/
}
Aqui o
ToList()
força a execução da consulta
imediatamente e materializa o resultado em uma
List<T>
✔ Agora a consulta original é enumerada apenas uma vez, durante o
.ToList(). Depois disso , Count e foreach trabalham
sobre a lista já materializada.
2.
Substitua LINQ por foreach em Loops Críticos
Códigos LINQ como
este são bonitos:
var total = usuarios.Where(u =>
u.EstaVerificado).Sum(u => u.Pontuacao);
Mas, se isso roda milhares de vezes por segundo, um simples foreach é
pode sermais rápido:
int total = 0;
foreach (var usuario in usuarios)
{
if (usuario.EstaVerificado)
total += usuario.Pontuacao;
}
✔ Evite alocações de lambdas, overhead de enumeradores e abstrações extra
Em loops extremamente frequentes e sensíveis à
performance, um
foreach pode ser mais eficiente que
LINQ, reduzindo o overhead de delegates e da abstração de enumeração.
3. Não Use .Count() Quando .Any() Resolve
Outro erro
comum:
if (produtos.Count()
> 0) // Pode percorrer toda a sequência!
Prefira:
if (produtos.Any()) // Para no primeiro item encontrado!
✔ Mais rápido, mais leve e sempre preferível quando você só quer saber se algo existe
Nota: O
Count()
pode precisar percorrer a sequência quando a fonte não fornece a quantidade
diretamente. Quando você só precisa saber se existe algum elemento,
Any()
expressa melhor a intenção e pode parar no primeiro elemento.
4. Evite Cadeias Complexas de LINQ
Códigos como este parecem elegantes:
var pontuacoesAltas = usuarios
.Where(u => u.EstaAtivo)
.Select(u => u.Pontuacao)
.Where(pontuacao => pontuacao > 80)
.ToList();
Neste código, o pipeline é avaliado quando o
ToList() é executado e os elementos passam pela cadeia de
operações durante essa enumeração.
Assim, uma cadeia LINQ pode introduzir iteradores e chamadas adicionais em
relação a um loop manual. Em código extremamente sensível à performance, um
foreach
pode reduzir esse overhead e dar ao compilador/runtime mais oportunidades de
otimização.
var pontuacoesAltas
= new List<int>();
foreach (var
usuario in usuarios)
{
if (usuario.EstaAtivo &&
usuario.Pontuacao > 80)
pontuacoesAltas.Add(usuario.Pontuacao);
}
✔ Essencial em sistemas de tempo real, servidores de jogos ou processamento
pesado.
5. Não Filtre os Mesmos Dados Várias Vezes
Já viu isso em código de produção?
var ativos =
usuarios.Where(u => u.EstaAtivo);
var
contagem = ativos.Count();
// Enumeração 1
var primeiro =
ativos.FirstOrDefault(); // Enumeração 2
Isso gera duas enumerações desnecessárias. O
Count()
enumera ativos
para obter a quantidade e FirstOrDefault()
faz uma nova enumeração para
obter o primeiro elemento.
Se a consulta for custosa — por exemplo, envolver processamento adicional ou acesso a uma fonte externa — essa repetição pode ser desnecessária.
Melhor:
var
ativos = usuarios.Where(u => u.EstaAtivo).ToList();
var contagem = ativos.Count;
// Já está em memória
var primeiro =
ativos.FirstOrDefault(); //
Sem nova enumeração
Aqui
ToList() executa a consulta
uma vez e materializa os
resultados. Depois,
Count usa a propriedade da lista e
FirstOrDefault()
acessa a lista sem precisar executar novamente o
Where.
✔ Itera apenas uma vez e reutiliza os resultado
Nota: usar
ToList() não é automaticamente
SEMPRE melhor. Você está trocando duas enumerações por
uma enumeração + materialização em
memória. Isso vale a pena quando você realmente precisa reutilizar os
resultados.
Resumo
.Any()
quando quiser apenas saber se existe algum elemento. foreach.
foreach
apenas por suposição. Na próxima vez que você analisar seu app e se perguntar "De onde vêm esses picos de memória?", verifique as suas cadeias LINQ.
A LINQ tem um custo e, em código crítico, esse custo pode importar.
E estamos conversados...
"Ai dos que ao mal chamam bem, e ao bem mal; que fazem das trevas luz, e da luz trevas; e fazem do amargo doce, e do doce amargo!" Isaias 5:20
Referências: