Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
System.Delegate e a palavra-chave
Este artigo aborda as classes no .NET que dão suporte a delegados e como elas são mapeadas para a delegate palavra-chave.
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 um código flexível em que diferentes métodos podem ser "conectados" para fornecer comportamentos diferentes.
Por exemplo, imagine que você tenha uma calculadora que possa executar operações em dois números. Em vez de codificar diretamente adição, subtração, multiplicação e divisão em métodos separados, você pode usar delegates (delegados) para representar qualquer operação que opere sobre dois números e retorne um resultado.
Definir tipos de delegado
Agora vamos ver como criar tipos de delegado usando a delegate palavra-chave. Quando você define 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 uma 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.
Vamos examinar um exemplo mais prático. Quando quiser classificar uma lista, você precisa informar ao algoritmo de classificação como comparar itens. Vamos ver como os delegados ajudam com o método List.Sort(). 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);
Esse delegado Comparison<T> pode conter referências a qualquer método que:
- Usa dois parâmetros do tipo
T - Retorna um
int(normalmente -1, 0 ou 1 para indicar "menor que", "igual a" ou "maior que")
Quando você define um tipo de delegado como este, o compilador gera automaticamente uma classe derivada da System.Delegate qual corresponde à sua 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 ele pode funcionar com qualquer tipo T. Para obter mais informações sobre genéricos, consulte classes e métodos genéricos.
Observe que, embora a sintaxe seja semelhante à declaração de uma variável, você está realmente declarando um novo tipo. Você pode definir tipos de delegado dentro de classes, diretamente dentro de namespaces ou até mesmo no namespace global.
Observação
Não é recomendável declarar tipos de delegado (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 corresponda à assinatura usada ao declarar o tipo delegado.
Declarar instâncias de delegados
Depois de definir o tipo delegado, você pode criar instâncias (variáveis) desse tipo. Considere isso como a criação de um "slot" em que 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 delegado que você definiu anteriormente) e o nome da variável é comparator. Neste ponto, comparator ainda não aponta para nenhum método: é como um slot vazio esperando para ser preenchido.
Você também pode declarar variáveis delegada 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 aponte 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 chame-a usando a sintaxe de chamada de método normal.
No entanto, essa linha de código faz uma suposição não segura: pressupõe que um método de destino tenha sido 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 posteriormente nesta série.
Atribuir, adicionar e remover destinos de invocação
Agora você sabe como definir tipos de delegado, declarar instâncias delegadas e invocar delegados. Mas como você realmente conecta um método a um delegado? É aqui que entra a delegação de tarefas.
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 delegado define.
Vamos ver 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 à Comparison<string> assinatura delegada:
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
Esse método recebe 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 da interface pública para usá-lo com um delegado.
Agora você pode passar esse método para o List.Sort() método:
phrases.Sort(CompareLength);
Observe que você usa o nome do método sem parênteses. Isso instrui o compilador a converter a referência do método em um delegado que pode ser chamado 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 de 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 de maneira direta. 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 de destino. No entanto, os objetos delegados podem dar suporte a listas de invocação que têm vários métodos de destino anexados a um único objeto delegado. Essa funcionalidade é particularmente útil para cenários de tratamento de eventos.
Classes Delegate e MulticastDelegate
Nos bastidores, os recursos delegados que você tem usado são criados em duas classes-chave no .NET Framework: Delegate e MulticastDelegate. Você geralmente não trabalha com essas classes diretamente, mas elas fornecem a base que faz os delegados funcionarem.
A classe System.Delegate e sua subclasse direta System.MulticastDelegate fornecem suporte à arquitetura para criar delegados, registrar métodos como alvos de delegados, e invocar todos os métodos que estão registrados em um delegado.
Aqui está um detalhe de design interessante: System.Delegate e System.MulticastDelegate não são eles mesmos tipos delegados que você pode usar. Em vez disso, elas servem como classes base para todos os tipos de delegado específicos que você cria. A linguagem C# impede que você herda diretamente dessas classes. Em vez disso, você deve usar a delegate palavra-chave.
Quando você usa a delegate palavra-chave para declarar um tipo delegado, o compilador C# cria automaticamente uma classe derivada de MulticastDelegate com sua assinatura específica.
Por que esse design?
Esse design tem suas raízes na primeira versão do C# e do .NET. A equipe de design tinha várias metas:
Segurança de tipos: a equipe queria garantir que a linguagem garantisse a segurança de tipos 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 no tempo de compilação.
Desempenho: ao fazer com que o compilador gere classes delegadas concretas que representam assinaturas de método específicas, o runtime pode otimizar invocações de delegado.
Simplicidade: os delegados foram incluídos na versão 1.0 do .NET, antes que os genéricos fossem introduzidos. O design precisava funcionar dentro das restrições da época.
A solução era fazer com que o compilador criasse as classes delegadas concretas que correspondem às assinaturas do método, garantindo a segurança do tipo ao ocultar a complexidade de você.
Trabalhando com métodos delegados
Mesmo que você não possa criar classes derivadas diretamente, você ocasionalmente usará métodos definidos nas classes Delegate e MulticastDelegate. Aqui estão as mais importantes para conhecer:
Cada delegado com o qual você trabalha é derivado de MulticastDelegate. Um delegado "multicast" significa que mais de um método de destino pode ser invocado ao chamar por meio de um delegado. O design original considerou fazer uma distinção entre delegados que só poderiam invocar um método versus delegados que poderiam invocar vários métodos. Na prática, essa distinção mostrou-se menos útil do que se pensava originalmente, portanto, todos os delegados no .NET dão suporte a vários métodos de destino.
Os métodos mais 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 (emboraasync/awaitagora seja preferencial)
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, há padrões que funcionam diretamente com esses métodos.
Resumo
Agora que você viu como a sintaxe da linguagem C# é mapeada para as classes .NET subjacentes, você está pronto para explorar como delegados fortemente tipados são usados, criados e invocados em cenários mais complexos.