Expressões lambda, delegados e eventos

Dica

Novo no desenvolvimento de software? Comece primeiro com os tutoriais de Introdução . Desenvolva habilidades básicas de tipos e métodos antes de usar expressões lambda.

Às vezes, você deseja passar uma pequena parte do comportamento, uma função, diretamente para outro método. Por exemplo, talvez você queira filtrar uma lista, mas a condição de filtragem muda dependendo da situação. Em vez de escrever um método nomeado separado para todas as condições possíveis, você passa a própria condição como um argumento.

Expressões lambda são o recurso C# que torna isso possível. Uma expressão lambda é uma função compacta embutida que você escreve sem dar um nome a ela. Use o operador => de seta para separar a lista de parâmetros do corpo:

x => x * 2

Ler da esquerda para a direita: x é o parâmetro de entrada, => significa "vai para" e x * 2 é o corpo. Ele calcula o valor retornado. Quando não houver parâmetros ou mais de um, embrulhe-os entre parênteses: () => 42 ou (left, right) => left + right.

Delegados dão suporte a expressões lambda

Para usar uma expressão lambda, o compilador C# precisa saber duas coisas: os tipos dos parâmetros e o tipo de retorno. Essa descrição, os tipos de parâmetro mais o tipo de retorno, é chamada de tipo delegado.

Um tipo de delegado é um tipo que representa uma assinatura de método. Uma variável de um tipo delegado pode conter qualquer método compatível, como uma expressão lambda ou um método nomeado, desde que os tipos de parâmetro e o tipo de retorno sejam compatíveis.

Você declara um tipo delegado com a delegate palavra-chave:

delegate int Transform(int value);

Esta declaração diz: "Transform é um tipo delegado para métodos que aceitam um int e retornam um int." Em seguida, você pode atribuir uma expressão lambda ou um método nomeado a uma variável desse tipo:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

Ambos doubler e squarer possuem um valor do tipo Transform. Você as chama exatamente como métodos regulares. O compilador verifica se o que você atribuir corresponde à assinatura declarada.

Tipos de delegado embutidos: Func e Action

Declarar um tipo de delegado personalizado para cada situação pode ser repetitivo. .NET fornece duas famílias de tipos delegados genéricos, Func e Action, que abrangem a maioria dos cenários, portanto, você raramente precisa usar a palavra-chave delegate por conta própria.

Ambas as famílias vêm em versões com parâmetros de tipo de entrada de zero a dezesseis, portanto, elas são dimensionadas para qualquer número de entradas. A principal diferença entre as duas famílias é:

  • System.Func<T,TResult> (e Func<T1, T2, TResult>assim por diante) representa um método que retorna um valor. O último parâmetro de tipo é sempre o tipo de retorno; todos os anteriores são tipos de entrada.
  • System.Action<T> (e Action<T1, T2>assim por diante) representa um método que não retorna nada (void). Todos os parâmetros de tipo são tipos de entrada. System.Action sem parâmetros de tipo representa um método sem entradas e sem valor retornado.

Por exemplo, Func<int, int, int> descreve um método com duas int entradas e um int resultado. Action<string> descreve um método com uma string entrada e nenhum valor retornado.

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

Use nomes de parâmetro descritivos em lambdas para que os leitores possam entender a intenção sem verificar o corpo completo do método.

Passar uma expressão lambda para um método

Quando um método declara um parâmetro Func ou um parâmetro Action, os chamadores passam uma expressão lambda que corresponde a esse tipo de delegado. O compilador verifica se os tipos de parâmetro do lambda e o tipo de retorno correspondem ao tipo de delegado declarado. Se eles não corresponderem, o código não será compilado.

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

O Filter método declara um Func<int, bool> parâmetro chamado predicate. O Func<int, bool> tipo informa aos chamadores a forma esperada: uma int entrada, um bool resultado. O chamador passa value => value % 2 == 0 como argumento. Esse padrão aparece em todo o LINQ e em muitas APIs de .NET.

Manter expressões lambda autossuficientes

Uma expressão lambda pode referenciar variáveis do código ao redor. Capturar significa que o lambda contém uma referência a uma variável declarada fora de seu próprio corpo. A combinação do lambda e das variáveis que ele captura é chamada de fechamento.

Quando você não precisar capturar nada, adicione o static modificador ao lambda. Um lambda estático só pode usar seus próprios parâmetros e valores declarados dentro de seu corpo. Ele não pode capturar variáveis locais ou o estado da instância do escopo delimitador.

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

Lambdas estáticas deixam a intenção clara e impedem capturas acidentais.

Usar parâmetros de descarte quando as entradas forem irrelevantes

Às vezes, uma assinatura delegada inclui parâmetros que você não precisa. Use o descarte _ para sinalizar essa escolha explicitamente.

Exemplos comuns incluem manipuladores de eventos em que você não usa sender ou EventArgs, retornos de chamada em que você só precisa de algumas de várias entradas e sobrecargas LINQ que fornecem um índice que você não usa.

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

Descartar parâmetros melhora a legibilidade porque mostra quais parâmetros são importantes.

Eventos fornecem notificações opcionais

Um evento é um mecanismo que um objeto (o editor) usa para notificar outros objetos (os assinantes) quando algo acontece. O editor não precisa saber quem está escutando ou quantos assinantes existem. Os assinantes escolhem participar.

Os eventos são baseados em delegados. Um evento é um campo delegado com restrições extras impostas pela palavra-chave event: código externo só pode se inscrever (+=) ou cancelar a inscrição (-=) no evento; somente a classe que declara o evento pode invocá-lo (dispará-lo).

A convenção de .NET para tipos de representante de eventos é System.EventHandler<TEventArgs>, em que T é o tipo de dados incluídos na notificação. Sua assinatura sempre tem dois parâmetros: o sender (objeto que gerou o evento) e os dados de evento do tipo T.

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

Explorando o código:

  • MessagePublisher declara event EventHandler<string>? MessagePublished. A event palavra-chave significa que os chamadores só podem assinar ou cancelar a assinatura. Eles não podem invocá-la diretamente.
  • publisher.MessagePublished += (_, message) => ... se inscreve com uma expressão lambda. O _ descarta o sender parâmetro porque esse manipulador não precisa dele.
  • publisher.Publish("Records updated") aciona o evento e executa todos os manipuladores inscritos.

A assinatura é opcional. ?.Invoke(...) no método Publish significa que o evento ocorre somente quando pelo menos um assinante está anexado. O publicador dispara o evento sem saber ou se importar se alguém está escutando.

Consulte também