Delegates e Lambdas: Comportamento como Dado

Delegates e Lambdas: Comportamento como Dado

A ideia que desmonta a aparente mágica do LINQ: comportamento pode ser tratado como dado. A aula apresenta delegates, que guardam um método numa variável, e lambdas, a forma concisa de escrever comportamento onde ele é necessário — e revela como os operadores de consulta funcionam.
Linguagem C#

• • 12 min de leitura

Chegamos à peça final do "motor" do LINQ, e é uma ideia genuinamente nova e poderosa, que amplia sua concepção do que é possível programar. Até agora, na sua mente, havia uma separação clara: dados (números, textos, objetos) eram uma coisa, guardados em variáveis; comportamento (o que os métodos fazem) era outra, escrito em blocos de código. Nesta aula, essa parede cai. Você aprenderá que, em C#, um pedaço de comportamento — uma pequena função — pode ser tratado como um dado: guardado numa variável, passado a um método como argumento. O mecanismo que permite isso chama-se delegate, e sua forma mais concisa é a lambda — aquela expressão n => n % 2 == 0 que vimos no LINQ. Ao final desta aula, o LINQ estará completamente desvendado, e você terá adquirido uma das ferramentas mais expressivas da programação.

A ideia surpreendente: comportamento como dado

Comecemos pela ideia central, que merece ser saboreada. Você já sabe passar dados a um método: Somar(5, 3) passa os números 5 e 3. A novidade é que você pode passar comportamento a um método — entregar-lhe não um valor, mas uma ação a ser executada. Imagine um método que aplica "alguma operação" a cada elemento de uma lista, mas qual operação é decidida por quem chama o método. Ou um método que filtra elementos segundo "algum critério", com o critério fornecido de fora. Em ambos os casos, o método recebe comportamento como parâmetro. Isso pode parecer estranho à primeira vista — passar uma "ação" como se fosse um valor —, mas é exatamente o que torna ferramentas como o LINQ possíveis, e você já viu isso em ação sem perceber: quando escreveu Where(n => n % 2 == 0), você passou um critério (um comportamento) ao operador Where.

O delegate: um "tipo de comportamento"

O mecanismo que permite tratar comportamento como dado é o delegate. Um delegate é, essencialmente, um "tipo" que representa uma forma de método — uma assinatura, isto é, o que o método recebe e o que devolve. Uma variável de um tipo delegate pode guardar qualquer método (ou lambda) que tenha aquela forma. Na prática moderna do C#, raramente você declara delegates você mesmo, porque a linguagem oferece dois tipos de delegate prontos que cobrem quase tudo, e é neles que focaremos: Func e Action.

A regra é simples: Func representa um comportamento que recebe valores e devolve um resultado; Action representa um comportamento que recebe valores mas não devolve nada (apenas executa). Veja como se guarda um comportamento numa variável usando Func:

// Um Func<int, int>: recebe um int, devolve um int.
// A variável 'dobrar' guarda um COMPORTAMENTO, não um valor comum.
Func<int, int> dobrar = n => n * 2;

// "Chamamos" o comportamento guardado como se fosse um método:
Console.WriteLine(dobrar(5));   // 10
Console.WriteLine(dobrar(10));  // 20

Observe o que acabou de acontecer: a variável dobrar não guarda um número — guarda uma ação (dobrar algo). E podemos "acionar" essa ação chamando dobrar(5), como se fosse um método. Isso é comportamento tratado como dado: uma ação armazenada numa variável e executada quando desejamos. No tipo Func<int, int>, os tipos entre os sinais de menor e maior descrevem a forma: o último é o tipo do resultado, e os anteriores são os tipos dos parâmetros — aqui, "recebe um int, devolve um int". Um Func<string, bool> receberia um texto e devolveria verdadeiro/falso; um Action<string> receberia um texto e não devolveria nada.

A lambda: escrevendo comportamento de forma concisa

Você notou que, ao criar o dobrar, escrevemos n => n * 2. Isso é uma lambda — uma forma concisa de escrever um pequeno comportamento (uma pequena função) sem lhe dar um nome nem a cerimônia completa de um método. É a mesma sintaxe que vimos no LINQ, e agora podemos entendê-la plenamente. A estrutura de uma lambda é: parâmetros => corpo. O símbolo => separa os parâmetros (à esquerda) do corpo, que produz o resultado (à direita):

Func<int, int> dobrar = n => n * 2;          // recebe n, devolve n * 2
Func<int, bool> ehPar = n => n % 2 == 0;     // recebe n, devolve se é par
Func<int, int, int> somar = (a, b) => a + b; // recebe a e b, devolve a soma
Action<string> saudar = nome => Console.WriteLine($"Olá, {nome}!");  // não devolve nada

Leia cada uma: n => n * 2 é "dado n, produza n * 2"; (a, b) => a + b é "dados a e b, produza a + b". Quando há um único parâmetro, os parênteses ao seu redor são opcionais (n => ...); quando há vários, são obrigatórios ((a, b) => ...). A lambda é apenas uma forma enxuta de escrever comportamento no exato ponto onde ele é necessário, sem a burocracia de definir um método nomeado separado. É por isso que ela é tão usada com o LINQ: você fornece o critério ou a transformação ali mesmo, de forma compacta.

Passando comportamento a métodos: o coração do LINQ

Agora unimos tudo. O verdadeiro valor de tratar comportamento como dado é passá-lo a um método, permitindo que o método execute um comportamento fornecido de fora. Veja um método que aplica um comportamento a cada elemento de uma lista, com o comportamento vindo de quem chama:

// Recebe uma lista E um comportamento (Func) a aplicar em cada elemento.
static List<int> Transformar(List<int> numeros, Func<int, int> operacao)
{
    List<int> resultado = new();
    foreach (int n in numeros)
    {
        resultado.Add(operacao(n));   // aplica o comportamento recebido a cada elemento
    }
    return resultado;
}

List<int> lista = new() { 1, 2, 3 };
var dobrados = Transformar(lista, n => n * 2);        // passa "dobrar" como comportamento
var quadrados = Transformar(lista, n => n * n);       // passa "elevar ao quadrado"

O método Transformar não sabe o que fará com cada número — isso é decidido pelo comportamento (Func) que ele recebe. Um mesmo Transformar serve para dobrar, elevar ao quadrado, negar, o que você quiser, bastando passar a lambda apropriada. E se isso lhe parece familiar, é porque é exatamente o que o Select do LINQ faz: ele recebe um comportamento de transformação (a lambda) e o aplica a cada elemento. Você acabou de reconstruir, em miniatura, o mecanismo do LINQ. Da mesma forma, o Where recebe um Func<int, bool> — um comportamento que, dado um elemento, diz se ele passa no filtro — e o aplica para decidir o que manter. As lambdas do LINQ são, o tempo todo, comportamentos entregues aos operadores.

O LINQ, enfim, desvendado

Reunamos as três peças das últimas aulas para desvendar o LINQ por completo, sem nenhuma mágica restante. Quando você escreve:

var resultado = numeros.Where(n => n % 2 == 0).Select(n => n * 2);

...o que acontece é: Where e Select são métodos genéricos (Aula #38), por isso funcionam com coleções de qualquer tipo; eles operam sobre qualquer coleção percorrível (IEnumerable, Aula #36), por isso aceitam listas, arrays, o que for; e cada um recebe um comportamento na forma de lambda (esta aula) — o critério de filtro (n => n % 2 == 0) e a regra de transformação (n => n * 2) — que aplicam a cada elemento. Três conceitos que você agora domina — genéricos, IEnumerable e lambdas — combinam-se para produzir a elegância do LINQ. O que parecia mágica na Aula #37 revela-se como a composição harmoniosa de peças compreensíveis. E esse é um dos grandes prazeres de aprender programação a fundo: ver a mágica dissolver-se em entendimento, e perceber que ferramentas sofisticadas são construídas a partir de ideias que, uma a uma, você é capaz de dominar.

A última peça conceitual entrou no lugar: comportamento pode ser tratado como dado. Um delegate é um tipo cujo valor é um método, o que permite guardar uma ação numa variável e passá-la adiante como se passa um número ou um texto. A lambda é a forma concisa de escrever essa ação exatamente onde ela é necessária, sem cerimônia.

Com isso, o mecanismo que parecia mágico ficou desmontado: os operadores de consulta recebem, como argumento, o comportamento que define o critério de filtragem ou de transformação. Não há sintaxe especial escondida na linguagem — há métodos comuns recebendo funções como parâmetro, e é essa ideia simples que sustenta toda a expressividade vista até aqui.

Fontes e leituras recomendadas

  • Delegates em C# — a introdução oficial ao mecanismo de tratar comportamento como dado; a base desta aula.
  • Expressões lambda — a sintaxe completa das lambdas, que o LINQ usa.
  • Func\<TResult\> — o delegate pronto para comportamentos que devolvem um resultado.
  • Action\<T\> — o delegate pronto para comportamentos que não devolvem nada.
  • Como o LINQ usa lambdas — a conexão entre lambdas e os operadores de consulta.

Exercícios

Exercício 1

Crie uma variável Func<int, int> chamada triplicar que multiplique um número por 3, e uma variável Func<int, bool> chamada ehPositivo que diga se um número é maior que zero. "Chame" ambas com alguns valores e exiba os resultados. Explique por que essas variáveis guardam "comportamento" e não valores comuns.

Ver resposta

✓ Resposta: Exemplo:

Func<int, int> triplicar = n => n * 3;
Func<int, bool> ehPositivo = n => n > 0;
Console.WriteLine(triplicar(4));    // 12
Console.WriteLine(ehPositivo(-2));  // False

Essas variáveis guardam "comportamento" e não valores comuns porque o que elas armazenam não é um número ou texto, mas uma ação a ser executada — "multiplicar por 3", "verificar se é positivo". Podemos "chamá-las" como se fossem métodos (triplicar(4)), fornecendo um valor e obtendo o resultado da ação. É comportamento tratado como dado: uma função guardada numa variável e executada quando desejamos.

Exercício 2

Explique, com suas palavras, a estrutura de uma lambda, usando (a, b) => a + b como exemplo. O que está à esquerda do =>, o que está à direita, e por que os parênteses são necessários neste caso mas opcionais em n => n * 2?

Ver resposta

✓ Resposta: Numa lambda, a estrutura é parâmetros => corpo. Em (a, b) => a + b, à esquerda do => estão os parâmetros — os dados de entrada a e b; à direita está o corpo — a expressão a + b, que produz o resultado. Os parênteses são necessários aqui porque há mais de um parâmetro (a e b), e o C# exige parênteses para agrupá-los. Em n => n * 2, há um único parâmetro (n), e nesse caso os parênteses ao seu redor são opcionais, podendo ser omitidos por concisão. A regra: parênteses obrigatórios com vários parâmetros (ou nenhum), opcionais com exatamente um.

Exercício 3

Escreva um método List<int> Filtrar(List<int> numeros, Func<int, bool> criterio) que devolva apenas os números que satisfazem o critério recebido. Chame-o passando uma lambda que selecione os números maiores que 5, e depois outra que selecione os pares. Explique como o mesmo método serve aos dois casos.

Ver resposta

✓ Resposta: Exemplo:

static List<int> Filtrar(List<int> numeros, Func<int, bool> criterio)
{
    List<int> resultado = new();
    foreach (int n in numeros)
        if (criterio(n)) resultado.Add(n);
    return resultado;
}
var grandes = Filtrar(new List<int> { 1, 8, 3, 10 }, n => n > 5);      // 8, 10
var pares = Filtrar(new List<int> { 1, 2, 3, 4 }, n => n % 2 == 0);    // 2, 4

O mesmo método serve aos dois casos porque ele não sabe qual é o critério — apenas recebe um comportamento (Func<int, bool>) e o aplica a cada elemento, mantendo os que fazem o critério devolver verdadeiro. O critério vem de fora, como um dado, então basta passar lambdas diferentes (n => n > 5 ou n => n % 2 == 0) para obter filtragens diferentes, sem alterar o método.

Exercício 4

Explique a diferença entre Func e Action, dando um exemplo de comportamento apropriado para cada um. Por que o exemplo de Action não usa return?

Ver resposta

✓ Resposta: Func representa um comportamento que devolve um resultado, enquanto Action representa um comportamento que não devolve nada, apenas executa uma ação. Exemplo de Func: Func<int, int> dobrar = n => n * 2 — recebe um número e devolve o dobro. Exemplo de Action: Action<string> exibir = texto => Console.WriteLine(texto) — recebe um texto e o exibe, sem produzir um valor de retorno. O exemplo de Action não usa return porque seu propósito é apenas fazer algo (exibir na tela), não calcular e devolver um valor; como não há resultado a entregar, não há o que retornar.

Exercício 5

Sem código: reunindo as três últimas aulas, explique por que a consulta numeros.Where(n => n > 3) funciona. Identifique o papel de cada um dos três conceitos — genéricos, IEnumerable e lambda — nessa única linha.

Ver resposta

✓ Resposta: A consulta numeros.Where(n => n > 3) funciona pela combinação de três conceitos. Os genéricos (Aula #38): o Where é um método genérico, por isso funciona com uma coleção de qualquer tipo — aqui, de inteiros — sem precisar de uma versão específica. O IEnumerable (Aula #36): o Where opera sobre qualquer coleção percorrível, então aceita a numeros seja ela uma List, um array ou outra coleção, pois todas cumprem esse contrato de "ser percorrível". A lambda (esta aula): n => n > 3 é um comportamento — um critério de filtro — passado ao Where como dado; o Where aplica esse comportamento a cada elemento para decidir quais manter. Juntos, os três fazem o Where receber o critério como um dado, aplicá-lo a cada inteiro da coleção percorrível, e devolver os que passam — tudo de forma genérica e reutilizável.

Comentários

Mais em Linguagem C#

LINQ: Consultando Dados com Elegância
LINQ: Consultando Dados com Elegância

A mudança de mentalidade que o LINQ traz: descrever o resultado desejado em…

Um Blog Completo com ASP.NET Core
Um Blog Completo com ASP.NET Core

Um site navegável completo em C#: um blog com Razor Pages, com listagem de…

Interfaces: Contratos de Comportamento
Interfaces: Contratos de Comportamento

A interface como contrato de comportamento: declara o que um tipo deve saber…