LINQ - Melhorando o código e o desempenho - I
  Hoje iremos recordar como a LINQ pode melhorar o desempenho das consultas na plataforma .NET.

A LINQ - Language Integrated Query mudou a forma como os desenvolvedores interagem com dados na plataforma .NET por oferecer uma sintaxe poderosa e expressiva que permite consultar diversas fontes de dados

 

 

Além de sua legibilidade e concisão, a LINQ apresenta melhorias de desempenho que se tornam particularmente evidentes na transição do código processual tradicional para sua abordagem declarativa.

 

Neste artigo, vamos examniar o impacto da LINQ no código e desempenho das consultas por meio de exemplos práticos, apresentando o código antes e depois da adoção do LINQ.

 

Vamos aquecer as turbinas considerarando um cenário em que queremos filtrar e manipular dados sem usar a LINQ. Vamos iniciar com uma tarefa comum:

 

'Filtrar uma lista de inteiros e depois elevar ao quadrado cada número que atenda a uma determinada condição.'

 

1- Abordagem tradicional sem usar a LINQ

 

List<int> mumeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> numerosAoQuadrado = new List<int>();
foreach (int numero in mumeros)
{
    if (numero % 2 == 0 && numero > 5)
    {
        numerosAoQuadrado.Add(numero * numero);
    }
}

 

Este código cria uma lista de números de 1 a 10, e então cria uma segunda lista chamada numerosAoQuadrado que contém os quadrados de todos os números pares maiores que 5 da lista original. Em seguida, imprime os números dessa segunda lista.

 

Embora o código acima atinja o resultado desejado, falta-lhe a elegância e expressividade que a LINQ traz para a mesa. Agora, vamos refazer a mesma tarefa usando LINQ.

 

List numeros = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var numerosAoQuadrado = numeros
     .Where(numero => numero % 2 == 0 && numero > 5)
    .Select(numero => numero * numero); 

Essas alterações simplificam o código e o tornam mais eficiente, utilizando as capacidades do LINQ para filtrar e mapear os números conforme necessário. Isso foi apenas uma amostra do que a LINQ pode fazer pelo seu código.

Vejamos agora como a LINQ usa a execução adiada ou Deferred Execution.

Deferred Execution

A LINQ introduz o conceito de execução adiada, o que significa que a execução de uma consulta é adiada até que os resultados reais sejam necessários. Isto pode levar a melhorias significativas de desempenho, especialmente ao lidar com grandes conjuntos de dados.

Exemplo:

List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var consulta = numeros.Where(num => num > 5).Select(num => num * 2);
foreach (var resultado in consulta)
{
    Console.WriteLine(resultado);
}

Neste exemplo, as operações Where e Select são adiadas até que o loop foreach itere sobre os resultados. Essa execução adiada minimiza cálculos desnecessários e melhora a eficiência do código.

Neste código a consulta LINQ está fazendo o seguinte:

  1. numeros.Where(num => num > 5): filtra a lista numeros para incluir apenas os números maiores que 5.

  2. .Select(num => num * 2): Aplica uma operação em cada elemento da lista filtrada. Para cada elemento, está multiplicando o valor por 2.

Assim, a variável consulta é uma coleção resultante dessas operações, e, ela contém os números maiores que 5 multiplicados por 2.

A seguir ocódigo itera sobre a consulta para imprimir cada resultado no console.

Este código tem algumas vantagens em relação ao código original:

  1. Clareza: A sintaxe do LINQ tende a ser mais clara e expressiva, tornando mais fácil entender o que está sendo feito com os dados.

  2. Menos código: Com LINQ, você pode realizar operações de filtro e projeção em uma única linha de código, ao invés de usar loops foreach separados, tornando o código mais conciso.

IQueryable para consultar banco de dados

Ao trabalhar com bancos de dados, a LINQ pode ser otimizado ainda mais usando a interface IQueryable. Esta interface permite a criação de consultas mais eficientes, pois podem ser traduzidas em consultas SQL otimizadas.

A interface IQueryable é fundamental para a otimização de consultas LINQ quando se trabalha com bancos de dados. Ela permite que as consultas LINQ sejam construídas de forma que o Entity Framework, por exemplo, possa traduzi-las diretamente em consultas SQL mais eficientes e otimizadas, executando partes da consulta diretamente no banco de dados em vez de trazer todos os dados para a aplicação e filtrar localmente.

Suponha que temos uma entidade Produto e queremos obter todos os produtos que estão em estoque e têm um preço superior a R$50,00. Podemos usar a LINQ e a interface IQueryable para criar uma consulta que seja traduzida em SQL otimizado.

using (var context = new ApplicationContext())
{
   // Consulta inicial, usando IQueryable
   IQueryable consulta = context.Produtos
       .Where(p => p.QuantidadeEmEstoque > 0) // Apenas produtos em estoque
       .Where(p => p.Preco > 50); // Apenas produtos com preço maior que $50

    // Podemos adicionar mais filtros, ordenações, projeções, etc. à consulta IQueryable
    // Sem executar a consulta ainda, apenas construindo a árvore de expressão
    var produtosFiltrados = consulta.ToList();

    // Aqui a consulta é traduzida em uma única consulta SQL otimizada
    // SELECT * FROM Produtos WHERE QuantidadeEmEstoque > 0 AND Preco > 50
    foreach (var produto in produtosFiltrados)
    {
       Console.WriteLine($"{produto.Nome} - {produto.Preco}");
    }
}

Neste exemplo, usamos a interface IQueryable para construir a consulta, adicionando vários critérios de filtro (Where) à medida que construímos a consulta. A consulta só é executada quando chamamos ToList() (ou métodos similares como FirstOrDefault(), Single(), etc.). No momento em que ToList() é chamado, a consulta é traduzida em uma única consulta SQL otimizada e é executada no banco de dados, retornando apenas os resultados desejados.

Isso ilustra como a interface IQueryable permite construir consultas de forma mais flexível e como o Entity Framework pode traduzir essas consultas em SQL eficiente para executar no banco de dados, minimizando a quantidade de dados trazidos para a aplicação e melhorando a performance.

Filtrando e Transformando Strings


Considere um cenário em que você tem uma lista de nomes e deseja filtrar os nomes que começam com a letra ‘J’ e transformar os nomes restantes em letras maiúsculas.

Abordagem tradicional sem LINQ:

 

List<string> nomes = new List<string> { "Jair", "Alice", "Benedito", "Janice", "Carlos" };
List<string> nomeFiltradosEmCaixaAlta = new List<string>();
foreach (string nome in nomes)
{
    if (!nome.StartsWith("J"))
    {
        nomeFiltradosEmCaixaAlta.Add(nome.ToUpper());
    }

 

Vejamos agora o mesmo código usando LINQ:


List nomes = new List { "Jair", "Alice", "Benedito", "Janice", "Carlos" };

        // Utilizando LINQ para filtrar e transformar em caixa alta
        List nomeFiltradosEmCaixaAlta = nomes
            .Where(nome => !nome.StartsWith("J"))
            .Select(nome => nome.ToUpper())
            .ToList();

Neste código otimizado, estamos usando os métodos de extensão Where e Select da LINQ:

Agrupamento

 

Agora digamos que você tenha uma lista de produtos, cada um com uma categoria e preço, e queira calcular o preço médio de cada categoria.

 

O código tradicional geralmente usado para tratar com este problema é visto abaixo:

 

List<Produto> produtos = GetProdutos(); 
Dictionary<string, double> precosMedios = new Dictionary<string, double>();
Dictionary<string, int> contaProdutos = new Dictionary<string, int>();
foreach (Produto produto in produtos)
{
    if (!precosMedios.ContainsKey(produto.Categoria))
    {
        precosMedios.Add(produto.Categoria, 0);
        contaProdutos.Add(produto.Categoria, 0);
    }
    precosMedios[produto.Categoria] += produto.Preco;
    contaProdutos[produto.Categoria]++;}
foreach (string categoria in precosMedios.Keys.ToList())
{
    precosMedios[categoria] /= contaProdutos[categoria];
}
Console.ReadKey();
static List<Produto> GetProdutos()
{
    List<Produto> produtos = new List<Produto>
        {
            new Produto { Categoria = "Eletrônicos", Preco = 1500.00 },
            new Produto { Categoria = "Eletrônicos", Preco = 1200.00 },
            new Produto { Categoria = "Roupas", Preco = 89.99 },
            new Produto { Categoria = "Roupas", Preco = 59.99 },
            new Produto { Categoria = "Livros", Preco = 29.99 }
        };
    return produtos;
}
public class Produto
{
    public string? Categoria { get; set; }
    public double Preco { get; set; }

 

Agora temos a seguir o código otimizado usando a LINQ :

 

List<Produto> produtos = new List<Produto>
        {
            new Produto { Categoria = "Eletrônicos", Preco = 1500.00 },
            new Produto { Categoria = "Eletrônicos", Preco = 1200.00 },
            new Produto { Categoria = "Roupas", Preco = 89.99 },
            new Produto { Categoria = "Roupas", Preco = 59.99 },
            new Produto { Categoria = "Livros", Preco = 29.99 }
        };
// Agrupando os produtos por categoria
var produtosAgrupadosPorCategoria = produtos.GroupBy(produto => produto.Categoria);
// Calculando o preço médio de cada categoria
var precosMedios = produtosAgrupadosPorCategoria
    .ToDictionary(
        grupo => grupo.Key,
        grupo => grupo.Average(produto => produto.Preco)
    );
foreach (var categoria in precosMedios)
{
    Console.WriteLine($"Categoria: {categoria.Key}, Preço Médio: {categoria.Value:C2}");
}
Console.ReadKey();
public class Produto
{
    public string? Categoria { get; set; }
    public double Preco { get; set; }
}

Explicando o código otimizado:

  1. produtos.GroupBy(produto => produto.Categoria): Agrupa os produtos por categoria, criando um conjunto de grupos onde cada grupo contém todos os produtos de uma categoria específica.

  2. ToDictionary(...): Converte o resultado do agrupamento em um dicionário onde a chave é a categoria e o valor é o preço médio daquela categoria.

Dessa forma, evitamos a necessidade de criar e atualizar manualmente dicionários separados para armazenar os totais e as contagens, e conseguimos calcular os preços médios diretamente usando a funcionalidade de agrupamento e média da LINQ. Isso torna o código mais legível, conciso e eficiente.

Neste exemplo, temos:

O código calculará os preços médios para cada categoria e os imprimirá no console. Ao executar o programa, você deve ver a seguinte saída:

 

Na segunda parte do artigo vamos continuar analisando outros exemplos de otimização com LINQ.

 

E estamos conversados...

"Estai, pois, firmes na liberdade com que Cristo nos libertou, e não torneis a colocar-vos debaixo do jugo da servidão."
Gálatas 5:1

Referências:


José Carlos Macoratti