DEV Community

Gabriel Oliveira
Gabriel Oliveira

Posted on

Clean Code: Otimizando a Comparação de Listas no C#

Quem nunca precisou comparar duas listas e descobrir quais itens estavam em uma mas faltavam na outra? 🤔

É um cenário clássico: você tem uma lista de dados que veio do banco de dados e outra que veio de uma API, e precisa sincronizar as duas. O instinto inicial de muitos desenvolvedores (inclusive no início da minha caminhada como dev 😅) é escrever laços de repetição (foreach) aninhados.

O problema? Embora funcione para listas pequenas, essa prática aumenta a complexidade do código e, pior, torna a leitura cansativa e a performance... bem, deixa a desejar.

O Cenário: Comparando Produtos

Imagine que você precisa sincronizar um catálogo de produtos. Você tem:

  • Uma lista de produtos do banco de dados
  • Uma lista de produtos da API externa
  • Necessidade de encontrar quais produtos são novos (estão na API mas não no BD)

❌ O Jeito Imperativo (Evite!)

public class NestedLoopComparisonService<T> : IListComparisonService<T>
{
    public IEnumerable<T> FindDifferences(IEnumerable<T> sourceList, IEnumerable<T> comparisonList)
    {
        ArgumentNullException.ThrowIfNull(sourceList);
        ArgumentNullException.ThrowIfNull(comparisonList);

        var differences = new List<T>();

        // WARNING: Loops aninhados = O(n²)!
        // Para cada item da lista source...
        foreach (var sourceItem in sourceList)
        {
            var foundInComparison = false;

            // ...percorremos TODA a comparison list
            foreach (var comparisonItem in comparisonList)
            {
                if (EqualityComparer<T>.Default.Equals(sourceItem, comparisonItem))
                {
                    foundInComparison = true;
                    break;
                }
            }

            if (!foundInComparison)
            {
                differences.Add(sourceItem);
            }
        }

        return differences;
    }
}
Enter fullscreen mode Exit fullscreen mode

Por que evitar?

  • ⚠️ Complexidade O(n × m) - exponencialmente lento
  • ⚠️ Para 1.000 itens = até 1.000.000 de comparações!
  • ⚠️ Código verboso e difícil de manter
  • ⚠️ Intenção não é clara

✅ O Jeito LINQ (Recomendado!)

Estudando LINQ, descobri uma forma elegante usando o método Except:

public class LinqExceptComparisonService<T> : IListComparisonService<T>
{
    private readonly IEqualityComparer<T>? _comparer;

    public LinqExceptComparisonService(IEqualityComparer<T>? comparer = null)
    {
        _comparer = comparer;
    }

    public IEnumerable<T> FindDifferences(IEnumerable<T> sourceList, IEnumerable<T> comparisonList)
    {        ArgumentNullException.ThrowIfNull(sourceList);
        ArgumentNullException.ThrowIfNull(comparisonList);
        // LINQ Except usa HashSet internamente = O(n + m)!
        return _comparer is null
            ? sourceList.Except(comparisonList)
            : sourceList.Except(comparisonList, _comparer);
    }
}
Enter fullscreen mode Exit fullscreen mode

Em uma única linha, resolvemos o problema e deixamos claro: "quero os itens que existem em A mas não em B".

O Impacto Real: Benchmarks 📊

Criei benchmarks com dados reais usando BenchmarkDotNet. Aqui estão os resultados comparando os dois métodos:

Dataset Nested Loop LINQ Except Diferença
100 itens ~41.0 µs ~0.5 µs 82x mais rápido
1.000 itens ~4.1 ms ~5.0 µs 820x mais rápido
10.000 itens ~410 ms ~50 µs 8.200x mais rápido

A diferença é brutal! Com 10.000 produtos, a abordagem imperativa leva 410 milissegundos, enquanto LINQ resolve em 50 microssegundos.

Exemplo Prático com Entidades Reais

Vamos usar a entidade Product real do projeto:

public class Product : IEquatable<Product>
{
    public int Id { get; init; }
    public string Name { get; init; }
    public decimal Price { get; init; }

    public bool Equals(Product? other)
    {
        if (other is null) return false;
        // Dois produtos são iguais se tiverem o mesmo ID
        return Id == other.Id;
    }

    public override int GetHashCode() => Id.GetHashCode();
}
Enter fullscreen mode Exit fullscreen mode

Usando nosso serviço:

var produtosAPI = await FetchProductsFromAPI();
var produtosBD = await FetchProductsFromDatabase();
var linqService = new LinqExceptComparisonService<Product>();

// Encontra produtos novos (que vieram da API mas não estão no BD)
var novosProdutos = linqService.FindDifferences(produtosAPI, produtosBD);

Console.WriteLine($"Encontrados {novosProdutos.Count()} novos produtos!");
Enter fullscreen mode Exit fullscreen mode

O Desafio da Referência de Memória

Ao trabalhar com tipos de referência (classes), o Except padrão compara as referências de memória. Por isso implementamos IEquatable<T> na classe Product, definindo que dois produtos são iguais se tiverem o mesmo Id.

A Solução Moderna: ExceptBy (.NET 6+)

Com o C# evoluindo, ganhamos ExceptBy, que permite comparar sem alterar a classe:

var novosProdutos = produtosAPI.ExceptBy(
    produtosBD.Select(p => p.Id),  // IDs que já existem
    p => p.Id                       // Usa o ID pra comparar
);
Enter fullscreen mode Exit fullscreen mode

Perfeito quando você não pode (ou não quer) implementar IEquatable<T>.

Conclusão

Escrever código não é apenas sobre fazer funcionar, é sobre comunicar intenção.

Ao substituir laços manuais por métodos semânticos como Except ou ExceptBy, você:

  • ✅ Reduz a complexidade de O(n²) para O(n+m)
  • ✅ Melhora a legibilidade drasticamente
  • ✅ Reduz a superfície de bugs
  • ✅ Facilita a manutenção futura
  • ✅ Ganha performance exponencial

🚀 Quer Explorar Mais?

O projeto completo está disponível no GitHub. Inclui:

  • ✅ Implementações completas de ambas as abordagens
  • ✅ Benchmarks detalhados com BenchmarkDotNet
  • ✅ Testes unitários com xUnit e FluentAssertions
  • ✅ Estrutura limpa seguindo princípios SOLID
  • ✅ Exemplos práticos com a entidade Product

Clique aqui para acessar o repositório!

Top comments (0)