20 delegados
20.1 General
Una declaración de delegado define una clase derivada de la clase System.Delegate
. Una instancia de delegado encapsula una lista de invocación, que es una lista de uno o varios métodos, cada uno de los cuales se conoce como una entidad invocable. Por ejemplo, una entidad invocable consta de una instancia y un método en esa instancia. En el caso de los métodos estáticos, una entidad invocable consta solo de un método . Invocar una instancia de delegado con un conjunto adecuado de argumentos hace que cada una de las entidades invocables del delegado se invoque con el conjunto de argumentos especificado.
Nota: Una propiedad interesante y útil de una instancia de delegado es que no conoce ni se preocupa por las clases de los métodos que encapsula; todo lo que importa es que esos métodos sean compatibles (§20.4) con el tipo del delegado. Esto hace que los delegados sean perfectamente adecuados para la invocación "anónima". nota final
20.2 Declaraciones de delegados
Un delegate_declaration es un type_declaration (§14.7) que declara un nuevo 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 se define en §23.2.
Es un error en tiempo de compilación para que el mismo modificador aparezca varias veces en una declaración de delegado.
Una declaración de delegado que proporciona un variant_type_parameter_list es una declaración de delegado genérica. Además, cualquier delegado anidado dentro de una declaración de clase genérica o una declaración de estructura genérica es una declaración de delegado genérico, ya que se proporcionarán argumentos de tipo para el tipo contenedor para crear un tipo construido (§8.4).
El new
modificador solo se permite en delegados declarados dentro de otro tipo, en cuyo caso especifica que dicho delegado oculta un miembro heredado por el mismo nombre, tal como se describe en §15.3.5.
Los public
modificadores , protected
, internal
y private
controlan la accesibilidad del tipo delegado. Según el contexto en el que se produzca la declaración de delegado, es posible que algunos de estos modificadores no se permitan (§7.5.2).
El nombre de tipo del delegado es el identificador.
Al igual que con los métodos (§15.6.1), si ref
está presente, el delegado devuelve por referencia; de lo contrario, si return_type es void
, el delegado devuelve-no-valor; de lo contrario, el delegado devuelve por valor.
El parameter_list opcional especifica los parámetros del delegado.
El return_type de una declaración de delegado returns-by-value o returns-no-value especifica el tipo del resultado, si existe, devuelto por el delegado.
El ref_return_type de una declaración de delegado return-by-ref especifica el tipo de la variable a la que hace referencia la variable_reference (§9.5) devuelta por el delegado.
El variant_type_parameter_list opcional (§18.2.3) especifica los parámetros de tipo para el propio delegado.
El tipo de valor devuelto de un tipo delegado será void
o seguro para la salida (§18.2.3.2).
Todos los tipos de parámetro de un tipo delegado serán seguros para la entrada (§18.2.3.2). Además, cualquier tipo de parámetro de salida o referencia también será seguro para la salida.
Nota: Los parámetros de salida deben ser seguros para la entrada debido a restricciones comunes de implementación. nota final
Además, cada restricción de tipo de clase, restricción de tipo de interfaz y restricción de parámetro de tipo en cualquier parámetro de tipo del delegado será segura para la entrada.
Los tipos delegados de C# son equivalentes de nombre, no equivalentes estructuralmente.
Ejemplo:
delegate int D1(int i, double d); delegate int D2(int c, double d);
Los tipos
D1
delegados yD2
son dos tipos diferentes, por lo que no son intercambiables, a pesar de sus firmas idénticas.ejemplo final
Al igual que otras declaraciones de tipos genéricos, se proporcionarán argumentos de tipo para crear un tipo delegado construido. Los tipos de parámetro y el tipo de valor devuelto de un tipo delegado construido se crean sustituyendo, por cada parámetro de tipo de la declaración de delegado, el argumento de tipo correspondiente del tipo construido.
La única manera de declarar un tipo de delegado es a través de un delegate_declaration. Cada tipo de delegado es un tipo de referencia derivado de System.Delegate
. Los miembros necesarios para cada tipo de delegado se detallan en §20.3. Los tipos delegados son implícitamente sealed
, por lo que no se permite derivar ningún tipo de un tipo delegado. Tampoco se permite declarar un tipo de clase no delegado derivado de System.Delegate
. System.Delegate
no es en sí mismo un tipo de delegado; es un tipo de clase del que se derivan todos los tipos delegados.
20.3 Miembros delegados
Cada tipo de delegado hereda miembros de la Delegate
clase como se describe en §15.3.4. Además, todos los tipos de delegado proporcionarán un método no genérico Invoke
cuya lista de parámetros coincida con el parameter_list en la declaración de delegado, cuyo tipo de valor devuelto coincida con el return_type o ref_return_type en la declaración de delegado, y para delegados devueltos por referencia cuyos ref_kind coincidan con los de la declaración de delegado. El Invoke
método debe ser al menos tan accesible como el tipo delegado contenedor. Llamar al Invoke
método en un tipo delegado es semánticamente equivalente a usar la sintaxis de invocación de delegado (§20.6).
Las implementaciones pueden definir miembros adicionales en el tipo de delegado.
Excepto para la creación de instancias, cualquier operación que se pueda aplicar a una clase o instancia de clase también se puede aplicar a una clase o instancia de delegado, respectivamente. En concreto, es posible acceder a los miembros del System.Delegate
tipo a través de la sintaxis de acceso de miembro habitual.
Compatibilidad con delegados 20.4
Un tipo de método o delegado M
es compatible con un tipo D
de delegado si se cumplen todas las siguientes condiciones:
D
yM
tienen el mismo número de parámetros, y cada parámetro deD
tiene el mismo modificador de parámetro por referencia que el parámetro correspondiente enM
.- Para cada parámetro de valor, existe una conversión de identidad (§10.2.2) o conversión de referencia implícita (§10.2.8) del tipo de parámetro en
D
al tipo de parámetro correspondiente enM
. - Para cada parámetro por referencia, el tipo de parámetro de
D
es el mismo que el tipo de parámetro enM
. - Se cumple una de las siguientes condiciones:
D
yM
son returns-no-valueD
yM
son devueltos por valor (§15.6.1, §20.2) y existe una conversión de identidad o referencia implícita del tipo deM
valor devuelto al tipo de valor devuelto deD
.D
yM
son tanto return-by-ref, existe una conversión de identidad entre el tipo de valor devuelto deM
y el tipo de valor devuelto deD
, y ambos tienen el mismo ref_kind.
Esta definición de compatibilidad permite la covarianza en el tipo de valor devuelto y la contravarianza en los tipos de parámetros.
Ejemplo:
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) {...} }
Los métodos y son compatibles
A.M1
con los tiposD1
de delegado yD2
, ya que tienen el mismo tipo de valor devuelto y lista de parámetros.B.M1
Los métodos ,B.M3
yB.M4
son incompatiblesB.M2
con los tiposD1
de delegado yD2
, ya que tienen diferentes tipos de valor devuelto o listas de parámetros. Los métodos yB.M6
son compatiblesB.M5
con el tipoD3
delegado .ejemplo final
Ejemplo:
delegate bool Predicate<T>(T value); class X { static bool F(int i) {...} static bool G(string s) {...} }
El método
X.F
es compatible con el tipoPredicate<int>
de delegado y el métodoX.G
es compatible con el tipoPredicate<string>
delegado .ejemplo final
Nota: El significado intuitivo de la compatibilidad de delegados es que un método es compatible con un tipo de delegado si cada invocación del delegado podría reemplazarse por una invocación del método sin infringir la seguridad del tipo, tratando parámetros opcionales y matrices de parámetros como parámetros explícitos. Por ejemplo, en el código siguiente:
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"); } }
El
Action<string>
tipo de delegado porque cualquier invocación de unAction<string>
delegado también sería una invocación válida delSi se cambiara la firma del
Print(object value, bool prependTimestamp = false)
, por ejemplo, elAction<string>
las reglas de esta cláusula.nota final
Creación de instancias del delegado 20.5
Una instancia de un delegado se crea mediante un delegate_creation_expression (§12.8.16.6), una conversión a un tipo delegado, combinación de delegados o eliminación de delegados. A continuación, la instancia de delegado recién creada hace referencia a una o varias de:
- Método estático al que se hace referencia en el delegate_creation_expression o
- Objeto de destino (que no puede ser
null
) y método de instancia al que se hace referencia en el delegate_creation_expression o - Otro delegado (§12.8.16.6).
Ejemplo:
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 } }
ejemplo final
El conjunto de métodos encapsulados por una instancia de delegado se denomina lista de invocación. Cuando se crea una instancia de delegado a partir de un único método, encapsula ese método y su lista de invocación contiene solo una entrada. Sin embargo, cuando se combinan dosnull
instancias no delegadas, sus listas de invocación se concatenan (en el orden en que el operando izquierdo y luego el operando derecho) forman una nueva lista de invocación, que contiene dos o más entradas.
Cuando se crea un nuevo delegado a partir de un único delegado, la lista de invocación resultante tiene solo una entrada, que es el delegado de origen (§12.8.16.6).
Los delegados se combinan con los operadores binarios (§12.10.5) y +=
(§12.21.4).+
Un delegado se puede quitar de una combinación de delegados, mediante los operadores binarios -
(§12.10.6) y -=
(§12.21.4). Los delegados se pueden comparar por igualdad (§12.12.9).
Ejemplo: en el ejemplo siguiente se muestra la creación de instancias de varios delegados y sus listas de invocación correspondientes:
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. } }
Cuando
cd1
ycd2
se crean instancias, cada uno encapsula un método. Cuandocd3
se crea una instancia, tiene una lista de invocación de dos métodos yM2
M1
, en ese orden.cd4
La lista de invocación de contieneM1
,M2
yM1
, en ese orden. Paracd5
, la lista de invocación contieneM1
,M2
,M1
,M1
yM2
, en ese orden.Al crear un delegado a partir de otro delegado con un delegate_creation_expression el resultado tiene una lista de invocación con una estructura diferente del original, pero que da como resultado que se invoquen los mismos métodos en el mismo orden. Cuando
td3
se crea a partir decd3
su lista de invocación tiene solo un miembro, pero ese miembro es una lista de los métodos yM2
esos métodosM1
se invocan mediantetd3
en el mismo orden en que se invocan mediantecd3
. Del mismo modo, cuandotd4
se crea una instancia de su lista de invocación tiene solo dos entradas, pero invoca los tres métodosM1
,M2
yM1
, en ese orden igualcd4
que lo hace.La estructura de la lista de invocación afecta a la resta de delegados. Delegue
cd6
, creado restandocd2
(que invocaM2
) decd4
(que invocaM1
a ,M2
yM1
) invocaM1
yM1
. Sin embargo, delegadotd6
, creado restandocd2
(que invocaM2
) detd4
(que invoca ,M2
yM1
) sigue invocandoM1
M1
,M2
yM1
, en ese orden, comoM2
no es una entrada única en la lista, sino un miembro de una lista anidada. Para obtener más ejemplos de combinación (y eliminación) de delegados, consulte §20.6.ejemplo final
Una vez creada la instancia, una instancia de delegado siempre hace referencia a la misma lista de invocación.
Nota: Recuerde que, cuando se combinan dos delegados o se quita uno de otro, los resultados de un nuevo delegado con su propia lista de invocación; las listas de invocación de los delegados combinados o quitados permanecen sin cambios. nota final
20.6 Invocación de delegado
C# proporciona una sintaxis especial para invocar un delegado. Cuando se invoca una instancia que nonull
es de delegado cuya lista de invocación contiene una entrada, invoca el método con los mismos argumentos que se dio y devuelve el mismo valor que el método al que se hace referencia. (Consulte §12.8.9.4 para obtener información detallada sobre la invocación de delegados). Si se produce una excepción durante la invocación de dicho delegado y esa excepción no se detecta dentro del método que se invocó, la búsqueda de una cláusula catch de excepción continúa en el método que llamó al delegado, como si ese método hubiera llamado directamente al método al que se hacía referencia.
La invocación de una instancia de delegado cuya lista de invocación contiene varias entradas, continúa invocando cada uno de los métodos de la lista de invocación, sincrónicamente, en orden. Cada método denominado se pasa el mismo conjunto de argumentos que se dio a la instancia de delegado. Si esta invocación de delegado incluye parámetros de referencia (§15.6.2.3.3), cada invocación de método se producirá con una referencia a la misma variable; los cambios realizados en esa variable por un método de la lista de invocación serán visibles para los métodos más abajo de la lista de invocación. Si la invocación del delegado incluye parámetros de salida o un valor devuelto, su valor final provendrá de la invocación del último delegado de la lista. Si se produce una excepción durante el procesamiento de la invocación de dicho delegado y esa excepción no se detecta dentro del método que se invocó, la búsqueda de una cláusula catch de excepción continúa en el método que llamó al delegado y no se invoca ningún método más abajo en la lista de invocación.
Si se intenta invocar una instancia de delegado cuyo valor da null
como resultado una excepción de tipo System.NullReferenceException
.
Ejemplo: en el ejemplo siguiente se muestra cómo crear instancias, combinar, quitar 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 se muestra en la instrucción
cd3 += cd1;
, un delegado puede estar presente en una lista de invocación varias veces. En este caso, simplemente se invoca una vez por repetición. En una lista de invocación como esta, cuando se quita ese delegado, la última aparición de la lista de invocación es la que realmente se ha quitado.Inmediatamente antes de la ejecución de la instrucción final,
cd3 -= cd1
;, el delegadocd3
hace referencia a una lista de invocación vacía. Intentar quitar un delegado de una lista vacía (o quitar un delegado no existente de una lista no vacía) no es un error.La salida generada es:
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
ejemplo final
ECMA C# draft specification