Partilhar via


System.Delegate e a delegate palavra-chave

Anterior

Este artigo aborda as classes no .NET que oferecem suporte a delegados e como eles são mapeados para a palavra-chave delegate .

O que são delegados?

Pense em um delegado como uma maneira de armazenar uma referência a um método, semelhante a como você pode armazenar uma referência a um objeto. Assim como você pode passar objetos para métodos, você pode passar referências de método usando delegados. Isso é útil quando você deseja escrever código flexível onde diferentes métodos podem ser "conectados" para fornecer comportamentos diferentes.

Por exemplo, imagine que você tem uma calculadora que pode executar operações em dois números. Em vez de codificar adição, subtração, multiplicação e divisão em métodos separados, você pode usar delegados para representar qualquer operação que pegue dois números e retorne um resultado.

Definir tipos de delegados

Agora vamos ver como criar tipos de delegados usando a delegate palavra-chave. Ao definir um tipo de delegado, você está essencialmente criando um modelo que descreve que tipo de métodos podem ser armazenados nesse delegado.

Você define um tipo de delegado usando sintaxe semelhante a uma assinatura de método, mas com a delegate palavra-chave no início:

// Define a simple delegate that can point to methods taking two integers and returning an integer
public delegate int Calculator(int x, int y);

Esse Calculator delegado pode conter referências a qualquer método que usa dois int parâmetros e retorna um int objeto.

Vejamos um exemplo mais prático. Quando você quiser classificar uma lista, você precisa dizer ao algoritmo de classificação como comparar itens. Vamos ver como os delegados ajudam com o List.Sort() método. A primeira etapa é criar um tipo de delegado para a operação de comparação:

// From the .NET Core library
public delegate int Comparison<in T>(T left, T right);

Este Comparison<T> delegado pode conter referências a qualquer método que:

  • Leva dois parâmetros do tipo T
  • Devolve um int (normalmente -1, 0 ou 1 para indicar "inferior a", "igual a" ou "maior que")

Quando se define um tipo de delegado desta forma, o compilador gera automaticamente uma classe derivada de System.Delegate que corresponde à assinatura. Essa classe lida com toda a complexidade de armazenar e invocar as referências de método para você.

O Comparison tipo delegado é um tipo genérico, o que significa que pode trabalhar com qualquer tipo T. Para obter mais informações sobre genéricos, consulte Classes e métodos genéricos.

Observe que, embora a sintaxe pareça semelhante à declaração de uma variável, na verdade você está declarando um novo tipo. Você pode definir tipos delegados dentro de classes, diretamente dentro de namespaces ou até mesmo no namespace global.

Observação

Não é recomendável declarar tipos delegados (ou outros tipos) diretamente no namespace global.

O compilador também gera manipuladores de adição e remoção para esse novo tipo para que os clientes dessa classe possam adicionar e remover métodos da lista de invocação de uma instância. O compilador impõe que a assinatura do método que está sendo adicionado ou removido corresponde à assinatura usada ao declarar o tipo de delegado.

Declarar instâncias de delegados

Depois de definir o tipo de delegado, você pode criar instâncias (variáveis) desse tipo. Pense nisso como criar um "slot" onde você pode armazenar uma referência a um método.

Como todas as variáveis em C#, você não pode declarar instâncias delegadas diretamente em um namespace ou no namespace global.

// Inside a class definition:
public Comparison<T> comparator;

O tipo dessa variável é Comparison<T> (o tipo de delegado definido anteriormente) e o nome da variável é comparator. Neste ponto, comparator ainda não aponta para nenhum método – é como um espaço vazio esperando para ser preenchido.

Você também pode declarar variáveis delegadas como variáveis locais ou parâmetros de método, assim como qualquer outro tipo de variável.

Invocar delegados

Depois de ter uma instância delegada que aponta para um método, você pode chamar (invocar) esse método por meio do delegado. Você invoca os métodos que estão na lista de invocação de um delegado chamando esse delegado como se fosse um método.

Veja como o Sort() método usa o delegado de comparação para determinar a ordem dos objetos:

int result = comparator(left, right);

Nessa linha, o código invoca o método anexado ao delegado. Você trata a variável delegada como se fosse um nome de método e a chama usando a sintaxe de chamada de método normal.

No entanto, esta linha de código faz uma suposição insegura: ela assume que um método de destino foi adicionado ao delegado. Se nenhum método tiver sido anexado, a linha acima fará com que um NullReferenceException seja lançado. Os padrões usados para resolver esse problema são mais sofisticados do que uma simples verificação nula e são abordados mais adiante nesta série.

Atribuir, adicionar e remover destinos de invocação

Agora você sabe como definir tipos de delegados, declarar instâncias delegadas e invocar delegados. Mas como você realmente conecta um método a um delegado? É aqui que entra a atribuição de delegado.

Para usar um delegado, você precisa atribuir um método a ele. O método atribuído deve ter a mesma assinatura (mesmos parâmetros e tipo de retorno) que o tipo de delegado define.

Vejamos um exemplo prático. Suponha que você queira classificar uma lista de cadeias de caracteres por seu comprimento. Você precisa criar um método de comparação que corresponda à assinatura do Comparison<string> delegado:

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

Esse método usa duas cadeias de caracteres e retorna um inteiro indicando qual cadeia de caracteres é "maior" (mais longa neste caso). O método é declarado como privado, o que é perfeitamente bom. Você não precisa que o método faça parte de sua interface pública para usá-lo com um delegado.

Agora você pode passar este método para o List.Sort() método:

phrases.Sort(CompareLength);

Observe que você usa o nome do método sem parênteses. Isto instrui o compilador a converter a referência do método em um delegado que pode ser invocado posteriormente. O Sort() método chamará seu CompareLength método sempre que precisar comparar duas cadeias de caracteres.

Você também pode ser mais explícito declarando uma variável delegada e atribuindo o método a ela:

Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

Ambas as abordagens realizam a mesma coisa. A primeira abordagem é mais concisa, enquanto a segunda torna a atribuição do delegado mais explícita.

Para métodos simples, é comum usar expressões lambda em vez de definir um método separado:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);

As expressões lambda fornecem uma maneira compacta de definir métodos simples em linha. O uso de expressões lambda para destinos delegados é abordado com mais detalhes em uma seção posterior.

Os exemplos até agora mostram delegados com um único método alvo. No entanto, os objetos delegados podem oferecer suporte a listas de invocação que têm vários métodos de destino anexados a um único objeto delegado. Esse recurso é particularmente útil para cenários de manipulação de eventos.

Classes Delegate e MulticastDelegate

Nos bastidores, os recursos de delegação que você tem usado são criados em duas classes principais no .NET framework: Delegate e MulticastDelegate. Você geralmente não trabalha com essas classes diretamente, mas elas fornecem a base que faz com que os delegados trabalhem.

A System.Delegate classe e sua subclasse System.MulticastDelegate direta fornecem o suporte de estrutura para criar delegados, registrar métodos como destinos delegados e invocar todos os métodos registrados com um delegado.

Aqui está um detalhe de design interessante: System.Delegate e System.MulticastDelegate não são tipos delegados que você pode usar. Em vez disso, eles servem como as classes base para todos os tipos de delegados específicos que você cria. A linguagem C# impede que você herde diretamente dessas classes — você deve usar a delegate palavra-chave em vez disso.

Quando usa a palavra-chave delegate para declarar um tipo de delegado, o compilador C# cria automaticamente uma classe derivada de MulticastDelegate com a sua assinatura específica.

Porquê este design?

Esse design tem suas raízes na primeira versão do C# e do .NET. A equipa de design tinha vários objetivos:

  1. Segurança do tipo: A equipe queria garantir que a linguagem aplicasse a segurança do tipo ao usar delegados. Isso significa garantir que os delegados sejam invocados com o tipo e o número corretos de argumentos, e que os tipos de retorno sejam verificados corretamente em tempo de compilação.

  2. Desempenho: Ao fazer com que o compilador gere classes delegadas concretas que representam assinaturas de método específicas, o tempo de execução pode otimizar as invocações de delegado.

  3. Simplicidade: Os delegados foram incluídos na versão 1.0 do .NET, que foi anterior à introdução dos genéricos. O projeto precisava trabalhar dentro das restrições do tempo.

A solução foi fazer com que o compilador criasse as classes delegadas concretas que correspondessem às suas assinaturas de método, garantindo a segurança do tipo enquanto ocultava a complexidade de você.

Trabalhando com métodos delegados

Mesmo que não se possa criar classes derivadas diretamente, ocasionalmente utilizará métodos definidos nas classes Delegate e MulticastDelegate. Aqui estão os mais importantes para saber:

Cada delegado com quem você trabalha é derivado do MulticastDelegate. Um delegado "multicast" significa que mais de um alvo de método pode ser invocado ao usar um delegado. O projeto original considerava fazer uma distinção entre delegados que só podiam invocar um método versus delegados que podiam invocar vários métodos. Na prática, essa distinção provou ser menos útil do que se pensava originalmente, de modo que todos os delegados no .NET oferecem suporte a vários métodos de destino.

Os métodos mais comumente usados ao trabalhar com delegados são:

  • Invoke(): Chama todos os métodos anexados ao delegado
  • BeginInvoke() / EndInvoke(): Usado para padrões de invocação assíncronos (embora async/await agora seja preferido)

Na maioria dos casos, você não chamará esses métodos diretamente. Em vez disso, você usará a sintaxe de chamada de método na variável delegada, conforme mostrado nos exemplos acima. No entanto, como você verá mais adiante nesta série, existem padrões que funcionam diretamente com esses métodos.

Resumo

Agora que você viu como a sintaxe da linguagem C# mapeia para as classes .NET subjacentes, está pronto para explorar como os delegados fortemente tipados são usados, criados e invocados em cenários mais complexos.

Próximo