Partilhar via


20 Delegados

20.1 Generalidades

Uma declaração de delegado 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 é referido como uma entidade chamável. Por exemplo, 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 em apenas um método. Invocar uma instância delegada com um conjunto apropriado de argumentos faz com que cada uma das entidades chamáveis do delegado seja invocada com o conjunto determinado de argumentos.

Nota: 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 invocações "anônimas". Nota final

20.2 Declarações dos delegados

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 no §23.2.

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

Uma declaração de delegado que fornece um 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 uma declaração struct genérica é ela própria uma declaração delegada genérica, uma vez que os argumentos de tipo para o tipo que contém devem ser fornecidos para criar um tipo construído (§8.4).

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

Os publicmodificadores , protected, internale private controlam a acessibilidade do tipo 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.

Tal como acontece com os métodos (§15.6.1), se ref estiver presente, o delegado retorna por ref, caso contrário, se return_type for void, o delegado retorna sem valor, caso contrário, o delegado retorna por valor.

O parameter_list opcional especifica os parâmetros do delegado.

O return_type de uma declaração de delegado de retorno por valor ou retorno sem valor especifica o tipo de resultado, se houver, retornado pelo delegado.

O ref_return_type de uma declaração de delegado de retorno por referência especifica o tipo da variável referenciada pelo variable_reference (§9.5) retornado pelo delegado.

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

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

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

Nota: Os parâmetros de saída devem ser seguros para entrada devido a restrições comuns de implementação. Nota final

Além disso, cada restrição de tipo de classe, restrição de tipo de interface e restrição de parâmetros de tipo em qualquer parâmetro de tipo do delegado deve ser segura para entradas.

Os tipos delegados em C# são equivalentes a nomes, não estruturalmente equivalentes.

Exemplo:

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

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

Exemplo final

Como outras declarações de tipo genéricas, argumentos de tipo devem ser dados 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. Cada tipo de delegado é um tipo de referência derivado de System.Delegate. Os membros necessários para cada tipo de delegado são detalhados no §20.3. Os tipos de delegados são implicitamente sealed, portanto, não é permitido derivar qualquer tipo de um tipo de delegado. Também não é permitido declarar um tipo de classe não delegada derivado de System.Delegate. System.Delegate não é, em si mesmo, um tipo delegado; É um tipo de classe do qual todos os tipos delegados são derivados.

20.3 Membros delegados

Cada tipo de delegado herda membros da classe, Delegate conforme descrito no §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 os delegados de retorno por referência cujo ref_kind corresponda ao da declaração de delegado. O Invoke método deve ser pelo menos tão acessível como o tipo de delegado que o contém. Chamar o Invoke método em um tipo de 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 para 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 delegada, respectivamente. Em particular, é possível acessar membros do System.Delegate tipo através da sintaxe de acesso de membro usual.

20.4 Compatibilidade de delegados

Um método ou tipo M de delegado é compatível com um tipo D de delegado se todos os itens a seguir 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, existe uma conversão de identidade (§10.2.2) ou conversão de referência implícita (§10.2.8) desde o 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 in D é o mesmo que o tipo de parâmetro em M.
  • Uma das seguintes afirmações é verdadeira:
    • D e M são ambos retornos-no-value
    • D e M são retornos por valor (§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 retornos por 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 a mesma ref_kind.

Esta definição de compatibilidade permite covariância no tipo de retorno e 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 D1 delegados e D2, uma vez que 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 D1 delegados e D2, uma vez que têm diferentes tipos de retorno ou listas de parâmetros. Os métodos B.M5 e B.M6 são ambos compatíveis com o tipo de delegado D3.

Exemplo final

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 Predicate<int> de delegado e o método X.G é compatível com o tipo Predicate<string>de delegado.

Exemplo final

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

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

Se a assinatura do Print método acima fosse alterada para Print(object value, bool prependTimestamp = false) , por exemplo, o Print método deixaria de ser compatível com Action<string> as regras desta cláusula.

Nota final

20.5 Delegar instanciação

Uma instância de um delegado é criada por um delegate_creation_expression (§12.8.16.6), uma conversão para um tipo de delegado, combinação de delegados ou remoção de delegado. A instância delegada recém-criada refere-se a um ou mais de:

  • 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.16.6).

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
    }
}

Exemplo final

O conjunto de métodos encapsulados por uma instância delegada é chamado de lista de invocação. Quando uma instância delegada é 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.16.6).

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 -= operadores (§12.21.4). Os delegados podem ser comparados em termos de igualdade (§12.12.9).

Exemplo: O exemplo a seguir 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. cd4A lista de invocação contém M1, M2e , nessa M1ordem. Para cd5, a lista de invocação contém M1, M2, , M1M1, 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 cd3 sua lista de invocação tem apenas um membro, mas esse membro é uma lista dos métodos M1 e M2 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, M2e M1, nessa ordem, assim como cd4 faz.

A estrutura da lista de invocação afeta a subtração do delegado. Delegar cd6, criado subtraindo cd2 (que invoca M2) de cd4 (que invoca M1, M2e M1) invoca M1 e M1. No entanto, delegar td6, criado subtraindo 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 mais exemplos de combinação (e remoção) de delegados, ver §20.6.

Exemplo final

Uma vez instanciada, uma instância delegada sempre se refere à mesma lista de invocação.

Nota: Lembre-se, quando dois delegados são combinados, ou um é removido do outro, um novo delegado resulta com sua própria lista de invocação, as listas de invocação dos delegados combinados ou removidos permanecem inalteradas. Nota final

20.6 Invocação do delegado

C# fornece sintaxe especial para invocar um delegado. Quando uma instância nãonull delegada, cuja lista de invocação contém uma entrada, é invocada, ela invoca o método one com os mesmos argumentos que lhe foram dados e retorna o mesmo valor que o método referido. (Ver §12.8.9.4 para obter informações detalhadas sobre a invocação de delegados.) 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 busca por uma cláusula catch de exceção continuará no método que chamou o delegado, como se esse método tivesse chamado diretamente o método ao qual esse 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, na ordem. Cada método assim chamado é passado o mesmo conjunto de argumentos que foi dado à instância delegada. Se tal 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 a essa variável por um método na lista de invocação serão visíveis para os métodos mais abaixo na lista de invocação. Se a invocação de delegado incluir parâmetros de saída ou um valor de retorno, seu valor final virá da invocação do último delegado na lista. Se ocorrer uma exceção durante o processamento da invocação de tal delegado, e essa exceção não for capturada dentro do método que foi invocado, a busca por uma cláusula catch 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 delegada cujo valor é null resulta em uma exceção do tipo System.NullReferenceException.

Exemplo: O exemplo a seguir 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
    }
}

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

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

A produção 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

Exemplo final