CodeGym /Cursos /C# SELF /Expressões Lambda e Delegates para Comparação

Expressões Lambda e Delegates para Comparação

C# SELF
Nível 30 , Lição 2
Disponível

1. Introdução

Imagina que você tem uma lista de estudantes e precisa ordenar uma vez por idade, depois por sobrenome, depois por média, mas só pra quem passou em todas as provas. Se pra cada comparação dessas você criar uma classe nova implementando IComparer<T>, seu projeto rapidinho vira uma bagunça cheia de classes comparadoras minúsculas. Não é nada prático: o código fica pesado e difícil de ler.

Pra essas situações, o C# tem uma solução bem mais elegante: você pode passar a lógica de comparação direto pro método Sort sem criar uma classe separada.

Pra isso, a gente vai usar delegates e seus irmãos compactos – as expressões lambda.

Delegates – nossos ajudantes flexíveis

Antes de cair nas lambdas, bora entender o que é um delegate. Falando fácil, um delegate é um tipo que representa uma referência pra um método. Meio filosófico, né? Pensa assim:

Imagina que você tem uma lista de tarefas, e algumas dessas tarefas são "instruções" ou "receitas". O delegate é tipo uma variável especial que pode guardar o endereço dessa "receita" (método). Aí, quando você precisa executar a tarefa, é só chamar a variável-delegate e ela "invoca" o método que tá guardado lá.

No C#, delegates são usados pra criar callbacks (chamar um método depois, geralmente como resposta a um evento), tratar eventos (tipo clicar num botão) e, claro, pra passar métodos como argumento pra outros métodos (pra um método poder chamar outro método), que é o que a gente vai usar pra ordenar.

O método List<T>.Sort() tem várias versões (overloads), e uma delas aceita um delegate especial chamado Comparison<T>.

2. Delegate Comparison<T>

O que é Comparison<T>?

Comparison<T> é um delegate já pronto do .NET, feito pra comparar dois objetos do mesmo tipo T. O "receitão" dele é assim: recebe dois objetos do tipo T (vamos chamar de x e y) e devolve um número inteiro (int):

  • Número negativo (tipo -1), se x "é menor" que y.
  • Zero (0), se x "é igual" a y.
  • Número positivo (tipo 1), se x "é maior" que y.

Essas são as mesmas regras do IComparable.CompareTo e do IComparer.Compare. Ou seja, a lógica é igual, só que agora a gente pode passar ela como uma "variável-método", não precisa de classe separada.

Bora ver um exemplo. Voltando pros nossos estudantes. Suponha que temos a classe Student:

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public double AverageGrade { get; set; }

    public Student(string firstName, string lastName, int age, double averageGrade)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        AverageGrade = averageGrade;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"Estudante: {FirstName} {LastName}, Idade: {Age}, Nota: {AverageGrade:F2}");
    }
}

Agora, pra ordenar a lista de estudantes por idade usando um delegate, dá pra criar um método estático separado, que bate certinho com a assinatura do Comparison<Student>:

public class Program
{
    // Método que bate com a assinatura do delegate Comparison<Student>
    // Vai comparar dois estudantes pela idade
    public static int CompareStudentsByAge(Student student1, Student student2)
    {
        // Usa o método CompareTo dos números,
        // que devolve -1, 0 ou 1 dependendo da comparação.
        return student1.Age.CompareTo(student2.Age);
    }

    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Aleksei", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2) // Dois estudantes com a mesma idade
        };

        Console.WriteLine("--- Lista de estudantes antes da ordenação ---");        
        foreach (var s in students)
            s.PrintInfo();

        Console.WriteLine("--- Ordenando estudantes por idade (usando delegate) ---");
        students.Sort(CompareStudentsByAge); //passa o método CompareStudentsByAge como parâmetro

        foreach (var s in students)
            s.PrintInfo();
    }
}

Destrinchando o código:

  1. Criamos um método estático CompareStudentsByAge, que recebe dois estudantes e devolve um int, seguindo o contrato do Comparison<Student>.
  2. No Main criamos a lista de estudantes.
  3. Quando chamamos students.Sort(CompareStudentsByAge);, a gente não executa o método CompareStudentsByAge() na hora! Só passamos a referência desse método. O List<T>.Sort() vai chamar nosso método CompareStudentsByAge quantas vezes precisar pra ordenar, passando pares diferentes de estudantes. É tipo dar o endereço de entrega pra alguém, não mandar o caminhão inteiro de uma vez.

Esse jeito é bem mais prático do que criar uma classe comparadora pra cada ordenação pequena. Mas dá pra deixar ainda mais enxuto!

3. Chegou a vez das Expressões Lambda

Mesmo criar um método separado, tipo CompareStudentsByAge, pode ser exagero se a lógica de comparação é simples e só vai ser usada uma ou duas vezes. Pra isso, o C# trouxe as expressões lambda (lambda expressions).

O que é uma expressão lambda? Basicamente, é um método anônimo ou, como eu gosto de brincar, "método sem teto". É um jeito de escrever um pedacinho de código (método) direto onde ele vai ser usado, sem declarar separado. Tipo rabiscar uma instrução num post-it e colar direto na tarefa, ao invés de escrever um manual inteiro.

O operador principal da lambda é o => (lê-se "vai pra" ou "seta"). Ele separa os parâmetros do corpo do método.

Sintaxe básica da expressão lambda

Digamos que você tem um delegate (referência pra método) e passa ele pra função Sort():

public static int CompareStudentsByAge(Student student1, Student student2)
{
   return student1.Age.CompareTo(student2.Age);
}

students.Sort(CompareStudentsByAge); //passa o método CompareStudentsByAge como parâmetro

Tem um jeito mais curto de escrever:

//passa um método anônimo como parâmetro
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );

Aqui, ao invés do nome do método, a gente coloca só o que interessa:

  • parâmetros: (Student student1, Student student2)
  • corpo do método: student1.Age.CompareTo(student2.Age)

Esse jeito compacto de escrever o método é a expressão lambda: (parâmetros) => expressão

Como funciona

O compilador C# quando vê uma expressão lambda no código, ele gera um método de verdade pra ela.

Por exemplo, se você tem esse código:

students.Sort( (s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade) );

O resultado da compilação vai ser mais ou menos assim:

public static int CompareStudents_Lambda123(Student s1, Student s2)
{
   return s2.AverageGrade.CompareTo(s1.AverageGrade);
}

students.Sort( CompareStudents_Lambda123 );

4. Exemplo de ordenação com expressão lambda

Bora reescrever nosso exemplo dos estudantes usando expressão lambda:

public class Program
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("Ivan", "Petrov", 20, 4.5),
            new Student("Maria", "Sidorova", 22, 4.8),
            new Student("Aleksei", "Ivanov", 19, 3.9),
            new Student("Elena", "Kozlova", 20, 4.2)
        };

        Console.WriteLine("--- Lista de estudantes antes da ordenação ---");        
        foreach (var s in students)
            s.PrintInfo();

        // Agora a lógica de comparação tá aqui, "no ato"
        Console.WriteLine("--- Ordenando estudantes por idade (usando expressão lambda) ---");
        students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));

        foreach (var s in students)
            s.PrintInfo();

        // Pra ordenar em ordem decrescente, só multiplicar o resultado por -1
        Console.WriteLine("\n--- Ordenando estudantes por média (decrescente) ---");
        // s2.CompareTo(s1) ao invés de s1.CompareTo(s2)
        students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));

        foreach (var s in students)
            s.PrintInfo();
    }
}

O que rolou aqui?

  1. students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
    • student1 e student2 são os parâmetros que o Sort vai passar pro nosso método anônimo (igual x e y no Comparison<T>).
    • => é o operador lambda.
    • student1.Age.CompareTo(student2.Age) é o corpo da lambda. Aqui, é só uma expressão, e o resultado já é o valor de retorno.
  2. Pra ordenar por média em ordem decrescente a gente só troca s1 e s2 no CompareTo. É o truque clássico pra inverter a ordem.

Por que isso é prático?

  • Enxuto: Não precisa criar métodos ou classes pra cada lógica de comparação pequena.
  • Fácil de ler: A lógica de comparação tá colada no Sort(), o que deixa o código mais claro, principalmente pra casos simples.
  • Flexível: Dá pra mudar as regras de ordenação rapidinho, "no ato".

5. Delegates e Lambdas – a dupla perfeita

Talvez você se pergunte: então expressão lambda é a mesma coisa que delegate, ou não?

Na real, expressão lambda é só açúcar sintático (syntax sugar) pra criar uma instância de delegate ou uma árvore de expressão (Expression Tree, isso é papo pra depois). Quando o compilador vê uma lambda, ele mesmo, "por baixo dos panos", transforma ela numa instância do delegate certo. No nosso caso, como o List<T>.Sort() espera um delegate Comparison<T>, o compilador entende que (student1, student2) => student1.Age.CompareTo(student2.Age) tem que virar um Comparison<Student>.

Ou seja, as expressões lambda deixam o código super enxuto, e os delegates são os "containers" que carregam esse código e fazem ele rodar. Eles trabalham juntos!

Quando usar cada um?

  • IComparable<T>: Use quando seu tipo tem um jeito natural, óbvio de ordenar. Por exemplo, se você ordena produtos e o principal critério é o código deles. Essa interface define a ordem "padrão".
  • IComparer<T>: Use quando você precisa de uma lógica de comparação reutilizável, pra várias vezes, mas não quer "sujar" a classe principal ou quando tem vários jeitos diferentes de ordenar. Por exemplo, um IComparer pra ordenar produtos por preço, outro por nome, e você usa eles em partes diferentes do programa.
  • Delegates (Comparison<T>) e Lambdas: São perfeitos pra ordenações pontuais, ad-hoc, quando a lógica é simples e não precisa de classe separada. Esse é o jeito mais comum e limpo pra maioria dos casos de ordenação em C#. Também é ótimo pra passar lógica pra outros métodos, tipo os de filtro (Find, FindAll) ou busca (FindIndex), que a gente já viu antes.
Característica IComparable<T> IComparer<T> Comparison<T> / Expressão Lambda
Onde é definido? Na própria classe T Em uma classe comparadora separada Pode ser método ou expressão anônima
Flexibilidade Ordem "natural" fixa Vários jeitos reutilizáveis de ordenar Ad-hoc (na hora), pra chamada específica
Boilerplate Pequeno, dentro da classe Médio (classe separada) Mínimo (principalmente com lambda)
Exemplo de uso
someList.Sort()
someList.Sort(new MyComparer())
someList.Sort((x, y) => x.Prop.CompareTo(y.Prop))
Facilidade de leitura Ótimo pra ordem natural Depende do nome do comparador Excelente pra comparações simples e específicas

6. Aplicação prática e o que vem pela frente

Expressões lambda não são só "açúcar sintático" pra ordenar. É uma ferramenta poderosa, usada em todo canto no código C# moderno. Você vai ver elas direto:

  • No LINQ (Language Integrated Query): Provavelmente o uso mais famoso das lambdas. LINQ deixa você escrever consultas tipo SQL em coleções, e as lambdas servem pra definir filtros, ordenações, projeções de dados. Logo a gente vai estudar LINQ, e você vai ver como as lambdas deixam ele super poderoso e prático.
  • No tratamento de eventos: Lambdas deixam fácil dizer o que fazer quando rolar um evento (tipo clicar num botão na interface).
  • Na programação assíncrona: Pra definir tarefas que vão rodar em paralelo.
  • Em várias APIs do .NET: Muitos métodos da biblioteca padrão aceitam delegates (e, portanto, lambdas) como parâmetro pra adicionar lógica flexível.

Ou seja, aprendendo expressões lambda, você não só melhora suas skills de ordenação, mas também dá um passo gigante pra entender o C# moderno e suas libs. É uma habilidade que todo recrutador curte e que vai te ajudar em qualquer projeto!

7. Erros comuns e pegadinhas

Quando você mexe com delegates e expressões lambda pra comparação, tem uns detalhes que vale ficar ligado:

Resultado de comparação errado: Lembra que o CompareTo ou sua lógica de comparação tem que devolver número negativo, zero ou positivo. Se você devolver outra coisa, a ordenação pode dar ruim ou até dar erro. O erro mais comum é iniciante devolver true ou false ao invés de int. O método Sort espera um número, porque ele precisa saber não só se os elementos são iguais, mas também qual é "maior".

Tratando valores null: Se sua coleção pode ter elementos null, tentar chamar método em null (tipo student1.Age.CompareTo(...) se student1 for null) vai dar NullReferenceException. Nesses casos, sua lógica de comparação tem que tratar null direitinho. A regra geral é: null é "menor" que qualquer valor não nulo. Se os dois são null, são iguais. Se só um é null, ele é "menor".

// Exemplo de tratamento de null na expressão lambda de comparação
students.Sort((s1, s2) => {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1; // null é menor que tudo
    if (s2 == null) return 1;  // não-null é maior que null
    return s1.Age.CompareTo(s2.Age); // Compara se os dois não são null
});

Felizmente, na vida real, coleções quase nunca têm null, mas é bom ficar esperto!

Performance: Apesar das lambdas serem super práticas, se você usar demais dentro de loops "quentes" ou com coleções gigantes, pode ter um impacto mínimo de performance comparado com classes IComparer super otimizadas, que talvez já foram testadas e perfiladas. Mas pra quase tudo do dia a dia, a diferença é irrelevante, e o ganho em clareza e simplicidade compensa muito.

Cadeias de comparação complexas: Como vimos no exemplo de ordenar por sobrenome, depois por nome, as lambdas deixam fácil encadear várias condições. Bem melhor do que escrever dez if numa linha só! O segredo é sempre checar o resultado da primeira comparação (lastNameComparison != 0) antes de passar pro próximo nível.

Expressões lambda e delegates são conceitos fundamentais no C#, que abrem as portas pra um estilo de programação mais flexível e funcional. Entender e saber usar eles vai deixar seu código muito mais limpo, eficiente e moderno. Vai testando, e logo você vai usar isso no automático!

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION