Compartir a través de


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 publicmodificadores , protected, internaly 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á voido 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 y D2 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 y M tienen el mismo número de parámetros, y cada parámetro de D tiene el mismo modificador de parámetro por referencia que el parámetro correspondiente en M.
  • 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 en M.
  • Para cada parámetro por referencia, el tipo de parámetro de D es el mismo que el tipo de parámetro en M.
  • Se cumple una de las siguientes condiciones:
    • Dy M son returns-no-value
    • D y M son devueltos por valor (§15.6.1, §20.2) y existe una conversión de identidad o referencia implícita del tipo de M valor devuelto al tipo de valor devuelto de D.
    • D y M son tanto return-by-ref, existe una conversión de identidad entre el tipo de valor devuelto de M y el tipo de valor devuelto de D, 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 tipos D1 de delegado y D2, ya que tienen el mismo tipo de valor devuelto y lista de parámetros.B.M1 Los métodos , B.M3y B.M4 son incompatibles B.M2con los tipos D1 de delegado y D2, ya que tienen diferentes tipos de valor devuelto o listas de parámetros. Los métodos y B.M6 son compatibles B.M5 con el tipo D3delegado .

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 tipo Predicate<int> de delegado y el método X.G es compatible con el tipo Predicate<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 Print método es compatible con el Action<string> tipo de delegado porque cualquier invocación de un Action<string> delegado también sería una invocación válida del Print método .

Si se cambiara la firma del Print método anterior a Print(object value, bool prependTimestamp = false) , por ejemplo, el Print método ya no sería compatible con Action<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 y cd2 se crean instancias, cada uno encapsula un método. Cuando cd3 se crea una instancia, tiene una lista de invocación de dos métodos y M2M1 , en ese orden. cd4La lista de invocación de contiene M1, M2y M1, en ese orden. Para cd5, la lista de invocación contiene M1, M2, M1, M1y M2, 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 de cd3 su lista de invocación tiene solo un miembro, pero ese miembro es una lista de los métodos y M2 esos métodos M1 se invocan mediante td3 en el mismo orden en que se invocan mediante cd3. Del mismo modo, cuando td4 se crea una instancia de su lista de invocación tiene solo dos entradas, pero invoca los tres métodos M1, M2y M1, en ese orden igual cd4 que lo hace.

La estructura de la lista de invocación afecta a la resta de delegados. Delegue cd6, creado restando cd2 (que invoca M2) de cd4 (que invoca M1a , M2y M1) invoca M1 y M1. Sin embargo, delegado td6, creado restando cd2 (que invoca M2) de td4 (que invoca , M2y M1) sigue invocando M1M1, M2 y M1, en ese orden, como M2 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 delegado cd3 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