Compartilhar via


20 Delegados

20.1 Geral

Uma declaração delegada define uma classe que é derivada da classe System.Delegate. Uma instância delegada encapsula uma lista de invocação, que é uma lista de um ou mais métodos, cada um dos quais é chamado de entidade chamável. Para métodos de instância, uma entidade chamável consiste em uma instância e um método nessa instância. Para métodos estáticos, uma entidade chamável consiste apenas em um método. A invocação de uma instância de delegado com um conjunto apropriado de argumentos faz com que cada uma das entidades chamáveis do delegado seja invocada com o conjunto de argumentos fornecido.

Observação: uma propriedade interessante e útil de uma instância delegada é que ela não conhece ou se preocupa com as classes dos métodos que encapsula, tudo o que importa é que esses métodos sejam compatíveis (§20.4) com o tipo do delegado. Isso torna os delegados perfeitamente adequados para a invocação "anônima". fim da observação

20.2 Declarações de delegado

Um delegate_declaration é um type_declaration (§14.7) que declara um novo tipo de delegado.

delegate_declaration
    : attributes? delegate_modifier* 'delegate' return_type delegate_header
    | attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
      delegate_header
    ;

delegate_header
    : identifier '(' parameter_list? ')' ';'
    | identifier variant_type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause* ';'
    ;
    
delegate_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier é definido em §23.2.

É um erro de tempo de compilação o fato de o mesmo modificador aparecer várias vezes em uma declaração de delegado.

Uma declaração de delegado que fornece uma variant_type_parameter_list é uma declaração de delegado genérica. Além disso, qualquer delegado aninhado dentro de uma declaração de classe genérica ou de uma declaração de estrutura genérica é, por si só, uma declaração de delegado genérico, uma vez que os argumentos de tipo para o tipo que o contém devem ser fornecidos para criar um tipo construído (§8.4).

O modificador new só é permitido em delegados declarados dentro de outro tipo, caso em que especifica que tal delegado oculta um membro herdado com o mesmo nome, como descrito em §15.3.5.

Os modificadores public, protected, internal e private controlam a acessibilidade do tipo de delegado. Dependendo do contexto em que a declaração de delegado ocorre, alguns desses modificadores podem não ser permitidos (§7.5.2).

O nome do tipo do delegado é identificador.

Assim como acontece com os métodos (§15.6.1), se ref estiver presente, o delegado returns-by-ref, caso contrário, se return_type for void, o delegado returns-no-value, caso contrário, o delegado returns-by-value.

O parameter_list opcional especifica os parâmetros do delegado.

O return_type de uma declaração de delegado returns-by-value ou returns-no-value especifica o tipo do resultado, se houver, retornado pelo delegado.

O ref_return_type de uma declaração de delegado returns-by-ref especifica o tipo da variável referenciada por variable_reference (§9.5) retornado pelo delegado.

O variant_type_parameter_list (§18.2.3) opcional especifica os parâmetros de tipo para o próprio delegado.

O tipo de retorno de um tipo delegado deve ser void, ou seguro para saída (§18.2.3.2).

Todos os tipos de parâmetros de um tipo de delegado devem ser seguros para entrada (§18.2.3.2). Além disso, todos os tipos de parâmetros de referência ou de saída também devem ser seguros para a saída.

Observação: os parâmetros de saída devem ser seguros para entrada devido a restrições comuns de implementação. fim da observação

Além disso, cada restrição de tipo de classe, restrição de tipo de interface e restrição de parâmetro de tipo em qualquer parâmetro de tipo do delegado deve ser à prova de entrada.

Os tipos de delegados em C# são equivalentes em termos de nome, mas não em termos estruturais.

Exemplo:

delegate int D1(int i, double d);
delegate int D2(int c, double d);

Os tipos de delegado D1 e D2 são dois tipos diferentes, portanto, não são intercambiáveis, apesar de suas assinaturas idênticas.

fim do exemplo

Como em outras declarações de tipo genérico, os argumentos de tipo devem ser fornecidos para criar um tipo de delegado construído. Os tipos de parâmetro e o tipo de retorno de um tipo de delegado construído são criados substituindo, para cada parâmetro de tipo na declaração de delegado, o argumento de tipo correspondente do tipo de delegado construído.

A única maneira de declarar um tipo de delegado é por meio de um delegate_declaration. Todo tipo de delegado é um tipo de referência derivado de System.Delegate. Os membros necessários para cada tipo de delegado estão detalhados em §20.3. Os tipos de delegados são implicitamente sealed, portanto, não é permitido derivar nenhum tipo de um tipo de delegado. Também não é permitido declarar um tipo de classe não delegado derivado de System.Delegate. System.Delegate não é em si um tipo de delegado, é um tipo de classe do qual todos os tipos de delegados são derivados.

20.3 Membros do delegado

Cada tipo de delegado herda membros da classe, Delegate conforme descrito em §15.3.4. Além disso, cada tipo de delegado deve fornecer um método não genérico Invoke cuja lista de parâmetros corresponda ao parameter_list na declaração de delegado, cujo tipo de retorno corresponda ao return_type ou ref_return_type na declaração de delegado e para delegados returns-by-ref cujo ref_kind corresponda ao da declaração de delegado. O método Invoke deve ser pelo menos tão acessível quanto o tipo de delegado que o contém. Chamar o método Invoke em um tipo delegado é semanticamente equivalente a usar a sintaxe de invocação de delegado (§20.6) .

As implementações podem definir membros adicionais no tipo de delegado.

Exceto pela instanciação, qualquer operação que possa ser aplicada a uma classe ou instância de classe também pode ser aplicada a uma classe ou instância de delegado, respectivamente. Em particular, é possível acessar membros do tipo System.Delegate por meio da sintaxe usual de acesso a membros.

20.4 Compatibilidade de delegado

Um método ou tipo de delegado M será compatível com um tipo de delegado D se todos os seguintes itens forem verdadeiros:

  • D e M têm o mesmo número de parâmetros, e cada parâmetro em D tem o mesmo modificador de parâmetro por referência que o parâmetro correspondente em M.
  • Para cada parâmetro de valor, uma conversão de identidade (§10.2.2) ou conversão de referência implícita (§10.2.8) existe a partir do tipo de parâmetro em D para o tipo de parâmetro correspondente em M.
  • Para cada parâmetro por referência, o tipo de parâmetro em D é o mesmo que o tipo de parâmetro em M.
  • Uma das seguintes é verdadeira:
    • D e M são sem valor.
    • D e M são returns-by-value (§15.6.1, §20.2) e existe uma conversão de identidade ou referência implícita do tipo de retorno de M para o tipo de retorno de D.
    • D e M são ambos returns-by-ref, existe uma conversão de identidade entre o tipo de retorno de M e o tipo de retorno de D, e ambos têm o mesmo ref_kind.

Essa definição de compatibilidade permite a covariância no tipo de retorno e a contravariância nos tipos de parâmetros.

Exemplo:

delegate int D1(int i, double d);
delegate int D2(int c, double d);
delegate object D3(string s);

class A
{
    public static int M1(int a, double b) {...}
}

class B
{
    public static int M1(int f, double g) {...}
    public static void M2(int k, double l) {...}
    public static int M3(int g) {...}
    public static void M4(int g) {...}
    public static object M5(string s) {...}
    public static int[] M6(object o) {...}
}

Os métodos A.M1 e B.M1 são compatíveis com os tipos de delegado D1 e D2, pois eles têm o mesmo tipo de retorno e lista de parâmetros. Os métodos B.M2, B.M3 e B.M4 são incompatíveis com os tipos de delegado D1 e D2, uma vez que eles têm diferentes tipos de retorno ou listas de parâmetros. Os métodos B.M5 e B.M6 são compatíveis com o tipo de delegado D3.

fim do exemplo

Exemplo:

delegate bool Predicate<T>(T value);

class X
{
    static bool F(int i) {...}
    static bool G(string s) {...}
}

O método X.F é compatível com o tipo de delegado Predicate<int> e o método X.G é compatível com o tipo de delegado Predicate<string>.

fim do exemplo

Observação: o significado intuitivo da compatibilidade de delegados é que um método é compatível com um tipo de delegado se toda invocação do delegado puder ser substituída por uma invocação do método sem violar a segurança de tipo, tratando parâmetros opcionais e matrizes de parâmetros como parâmetros explícitos. Por exemplo, no seguinte código:

delegate void Action<T>(T arg);

class Test
{
    static void Print(object value) => Console.WriteLine(value);

    static void Main()
    {
        Action<string> log = Print;
        log("text");
    }
}

O método Print é compatível com o tipo de delegado Action<string> porque qualquer invocação de um delegado Action<string> também seria uma invocação válida do método Print.

Se a assinatura do método Print acima fosse alterada para Print(object value, bool prependTimestamp = false), por exemplo, o método Print não seria mais compatível com Action<string> pelas regras desta subcláusula.

fim da observação

20.5 Instanciação de delegado

Uma instância de um delegado é criada por um delegate_creation_expression (§12.8.17.5), uma conversão para um tipo de delegado, combinação de delegados ou remoção de delegado. A instância do delegado recém-criado faz referência a um ou mais dos:

  • O método estático referenciado no delegate_creation_expression ou
  • O objeto de destino (que não pode ser null) e o método de instância referenciado no delegate_creation_expression ou
  • Outro delegado (§12.8.17.5).

Exemplo:

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public void M2(int i) {...}
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1); // Static method
        C t = new C();
        D cd2 = new D(t.M2); // Instance method
        D cd3 = new D(cd2);  // Another delegate
    }
}

fim do exemplo

O conjunto de métodos encapsulados por uma instância de delegado é chamado de lista de invocação. Quando uma instância de delegado é criada a partir de um único método, ela encapsula esse método e sua lista de invocação contém apenas uma entrada. No entanto, quando duas instâncias nãonull delegadas são combinadas, suas listas de invocação são concatenadas, na ordem do operando esquerdo e depois do operando direito, para formar uma nova lista de invocação, que contém duas ou mais entradas.

Quando um novo delegado é criado a partir de um único delegado, a lista de invocação resultante tem apenas uma entrada, que é o delegado de origem (§12.8.17.5).

Os delegados são combinados usando o binário + (§12.10.5) e operadores += (§12.21.4). Um delegado pode ser removido de uma combinação de delegados, usando o binário - (§12.10.6) e -= os operadores (§12.21.4). Os delegados podem ser comparados quanto à igualdade (§12.12.9).

Exemplo: o seguinte exemplo mostra a instanciação de vários delegados e suas listas de invocação correspondentes:

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public static void M2(int i) {...}
}

class Test
{
    static void Main() 
    {
        D cd1 = new D(C.M1); // M1 - one entry in invocation list
        D cd2 = new D(C.M2); // M2 - one entry
        D cd3 = cd1 + cd2;   // M1 + M2 - two entries
        D cd4 = cd3 + cd1;   // M1 + M2 + M1 - three entries
        D cd5 = cd4 + cd3;   // M1 + M2 + M1 + M1 + M2 - five entries
        D td3 = new D(cd3);  // [M1 + M2] - ONE entry in invocation
                             // list, which is itself a list of two methods.
        D td4 = td3 + cd1;   // [M1 + M2] + M1 - two entries
        D cd6 = cd4 - cd2;   // M1 + M1 - two entries in invocation list
        D td6 = td4 - cd2;   // [M1 + M2] + M1 - two entries in invocation list,
                             // but still three methods called, M2 not removed.
   }
}

Quando cd1 e cd2 são instanciados, cada um deles encapsula um método. Quando cd3 é instanciado, ele tem uma lista de invocação de dois métodos, M1 e M2, nessa ordem. Alista de invocação de cd4 contém M1, M2 e M1, nessa ordem. Para cd5, a lista de invocação contém M1, M2, M1, M1 e M2, nessa ordem.

Ao criar um delegado de outro delegado com um delegate_creation_expression o resultado tem uma lista de invocação com uma estrutura diferente da original, mas que resulta nos mesmos métodos sendo invocados na mesma ordem. Quando td3 é criado a partir de sua lista de invocação, cd3 tem apenas um membro, mas esse membro é uma lista dos métodos M1 e M2 e esses métodos são invocados por td3 na mesma ordem em que são invocados por cd3. Da mesma forma quando td4 é instanciado, sua lista de invocação tem apenas duas entradas, mas invoca os três métodos M1, M2 e M1, nessa ordem, assim como cd4 faz.

A estrutura da lista de invocações afeta a subtração de delegados. Delegado cd6, criado ao subtrair cd2 (que invoca M2) de cd4 (que invoca M1, M2, e M1) invoca M1 e M1. No entanto, delegado td6, criado ao subtrair cd2 (que invoca M2) de td4 (que invoca M1, M2, e M1) ainda invoca M1, M2 e M1, nessa ordem, como M2 não é uma única entrada na lista, mas um membro de uma lista aninhada. Para obter mais exemplos de combinação (bem como de remoção) de delegados, consulte §20.6.

fim do exemplo

Depois de instanciada, uma instância de delegado sempre se refere à mesma lista de invocação.

Observação: lembre-se de que, quando dois delegados são combinados ou um é removido de outro, resulta um novo delegado com sua própria lista de invocação, enquanto as listas de invocação dos delegados combinados ou removidos permanecem inalteradas. fim da observação

20.6 Invocação de delegado

O C# fornece sintaxe especial para invocar um delegado. Quando uma instância de delegado nãonull cuja lista de invocação contém uma entrada é invocada, ela invoca o método com os mesmos argumentos fornecidos e retorna o mesmo valor que o método referenciado. (Consulte §12.8.10.4 para obter informações detalhadas sobre invocação de delegado.) Se ocorrer uma exceção durante a invocação de tal delegado e essa exceção não for capturada dentro do método que foi invocado, a pesquisa de uma cláusula de captura de exceção continuará no método que chamou o delegado, como se esse método tivesse chamado diretamente o método ao qual aquele delegado se referia.

A invocação de uma instância delegada cuja lista de invocação contém várias entradas prossegue invocando cada um dos métodos na lista de invocação de forma síncrona, em ordem. Cada método assim chamado recebe o mesmo conjunto de argumentos que foi dado à instância de delegado. Se essa invocação de delegado incluir parâmetros de referência (§15.6.2.3.3), cada invocação de método ocorrerá com uma referência à mesma variável, as alterações nessa variável por um método na lista de invocação serão visíveis para métodos mais abaixo na lista de invocação. Se a invocação do delegado incluir parâmetros de saída ou um valor de retorno, seu valor final virá da invocação do último delegado da lista. Se ocorrer uma exceção durante o processamento da invocação de tal delegado e essa exceção não for capturada no método que foi invocado, a busca por uma cláusula de captura de exceção continuará no método que chamou o delegado, e quaisquer métodos mais abaixo na lista de invocação não serão invocados.

A tentativa de invocar uma instância de delegado cujo valor é null resulta em uma exceção do tipo System.NullReferenceException.

Exemplo: o seguinte exemplo mostra como instanciar, combinar, remover e invocar delegados:

delegate void D(int x);

class C
{
    public static void M1(int i) => Console.WriteLine("C.M1: " + i);

    public static void M2(int i) => Console.WriteLine("C.M2: " + i);

    public void M3(int i) => Console.WriteLine("C.M3: " + i);
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1);
        cd1(-1);             // call M1
        D cd2 = new D(C.M2);
        cd2(-2);             // call M2
        D cd3 = cd1 + cd2;
        cd3(10);             // call M1 then M2
        cd3 += cd1;
        cd3(20);             // call M1, M2, then M1
        C c = new C();
        D cd4 = new D(c.M3);
        cd3 += cd4;
        cd3(30);             // call M1, M2, M1, then M3
        cd3 -= cd1;          // remove last M1
        cd3(40);             // call M1, M2, then M3
        cd3 -= cd4;
        cd3(50);             // call M1 then M2
        cd3 -= cd2;
        cd3(60);             // call M1
        cd3 -= cd2;          // impossible removal is benign
        cd3(60);             // call M1
        cd3 -= cd1;          // invocation list is empty so cd3 is null
        // cd3(70);          // System.NullReferenceException thrown
        cd3 -= cd1;          // impossible removal is benign
    }
}

Conforme mostrado na instrução cd3 += cd1;, um delegado pode estar presente em uma lista de invocação várias vezes. Nesse caso, ele é simplesmente invocado uma vez por ocorrência. Em uma lista de invocação como essa, quando esse delegado é removido, a última ocorrência na lista de invocação é a que foi realmente removida.

Imediatamente antes da execução da instrução final, cd3 -= cd1, o delegado cd3 refere-se a uma lista de invocação vazia. A tentativa de remover um delegado de uma lista vazia (ou de remover um delegado inexistente de uma lista não vazia) não é um erro.

A saída produzida é:

C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60

fim do exemplo