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 public
modificadores , protected
, internal
e 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 void
a 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 eD2
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
eM
têm o mesmo número de parâmetros, e cada parâmetro emD
tem o mesmo modificador de parâmetro por referência que o parâmetro correspondente emM
.- 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 emM
. - Para cada parâmetro por referência, o tipo de parâmetro in
D
é o mesmo que o tipo de parâmetro emM
. - Uma das seguintes afirmações é verdadeira:
D
eM
são ambos retornos-no-valueD
eM
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 deM
para o tipo de retorno deD
.D
eM
são ambos retornos por ref, existe uma conversão de identidade entre o tipo de retorno deM
e o tipo de retorno deD
, 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
eB.M1
são compatíveis com os tiposD1
delegados eD2
, uma vez que eles têm o mesmo tipo de retorno e lista de parâmetros. Os métodosB.M2
,B.M3
, eB.M4
são incompatíveis com os tiposD1
delegados eD2
, uma vez que têm diferentes tipos de retorno ou listas de parâmetros. Os métodosB.M5
eB.M6
são ambos compatíveis com o tipo de delegadoD3
.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 tipoPredicate<int>
de delegado e o métodoX.G
é compatível com o tipoPredicate<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
Action<string>
tipo de delegado porque qualquer invocação de umAction<string>
delegado também seria uma invocação válida doSe a assinatura do
Print(object value, bool prependTimestamp = false)
, por exemplo, oAction<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
ecd2
são instanciados, cada um deles encapsula um método. Quandocd3
é instanciado, ele tem uma lista de invocação de dois métodos,M1
eM2
, nessa ordem.cd4
A lista de invocação contémM1
,M2
e , nessaM1
ordem. Paracd5
, a lista de invocação contémM1
,M2
, ,M1
M1
, eM2
, 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 decd3
sua lista de invocação tem apenas um membro, mas esse membro é uma lista dos métodosM1
eM2
esses métodos são invocados portd3
na mesma ordem em que são invocados porcd3
. Da mesma forma, quandotd4
é instanciado, sua lista de invocação tem apenas duas entradas, mas invoca os três métodosM1
,M2
eM1
, nessa ordem, assim comocd4
faz.A estrutura da lista de invocação afeta a subtração do delegado. Delegar
cd6
, criado subtraindocd2
(que invocaM2
) decd4
(que invocaM1
,M2
eM1
) invocaM1
eM1
. No entanto, delegartd6
, criado subtraindocd2
(que invocaM2
) detd4
(que invocaM1
,M2
, eM1
) ainda invocaM1
,M2
eM1
, nessa ordem, comoM2
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 delegadocd3
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
ECMA C# draft specification