Compartir vía


21 Delegados

21.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 (§21.4) con el tipo del delegado. Esto hace que los delegados sean perfectamente adecuados para la invocación "anónima". nota final

21.2 Declaraciones de delegados

Una 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 §24.2.

Es un error en tiempo de compilación porque el mismo modificador aparece varias veces en una declaración de delegado.

Una declaración de delegado que proporciona una 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érica, ya que se proporcionarán argumentos de tipo para el tipo contenedor para crear un tipo construido (sección 8.4).

El modificador new solo se permite en delegados declarados dentro de otro tipo, en cuyo caso especifica que dicho delegado oculta un miembro heredado con el mismo nombre, tal como se describe en §15.3.5.

Los modificadores public, protected, internal y private controlan la accesibilidad del tipo de delegado. En función del 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 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 no devuelve ningún valor; de lo contrario, el delegado devuelve por valor.

La parameter_list opcional especifica los parámetros del delegado.

El return_type de una declaración de delegado que devuelve por valor o que no devuelve ningún valor especifica el tipo del resultado, si existe, que devuelve el delegado.

El ref_return_type de una declaración de delegado que devuelve por referencia especifica el tipo de la variable a la que hace referencia variable_reference (§9.5) que devuelve el delegado.

El variant_type_parameter_list opcional (§19.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 (§19.2.3.2).

Todos los tipos de parámetro de un tipo delegado serán seguros para la entrada (§19.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 de delegado será segura para la entrada.

Los tipos de delegados de C# son equivalentes en su nombre, no en su estructura.

Ejemplo:

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

Los tipos de delegados D1 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 de delegado construido. Los tipos de parámetros y el tipo de valor devuelto de un tipo de 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 una 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 §21.3. Los tipos de delegados son implícitamente sealed, por lo que no se permite derivar ningún tipo de un tipo de 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 de delegados.

21.3 Miembros delegados

Cada tipo de delegado hereda miembros de la clase Delegate de la forma que se describe en §15.3.4. Además, todos los tipos de delegados proporcionarán un método Invoke no genérico cuya lista de parámetros coincida con la 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 método Invoke debe ser al menos tan accesible como el tipo de delegado que lo contiene. Llamar al Invoke método en un tipo delegado es semánticamente equivalente a usar la sintaxis de invocación de delegado (§21.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 tipo System.Delegate mediante la sintaxis de acceso de miembros habitual.

Compatibilidad con delegados 21.4

Un tipo de método o delegado M es compatible con un tipo de delegado D 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 de M.
  • Una de las siguientes condiciones se cumple:
    • D y M son returns-no-value.
    • D y M son devueltos por valor (§15.6.1, §21.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 ambos devoluciones por referencia; existe una conversión de identidad entre el tipo de devolución de M y el tipo de devolución de D, y ambos tienen el mismo ref_kind.

Esta definición de compatibilidad permite la covarianza en el tipo de devolución 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 A.M1 y B.M1 son compatibles con los tipos de delegados D1 y D2, ya que tienen el mismo tipo de devolución y lista de parámetros. Los métodos B.M2, B.M3 y B.M4 son incompatibles con los tipos de delegado D1 y D2, ya que tienen diferentes tipos de devoluciones o listas de parámetros. Los métodos B.M5 y B.M6 son compatibles con el tipo de delegado D3.

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 de delegado Predicate<int> y el método X.G es compatible con el tipo de delegado Predicate<string>.

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

Si se cambiara la firma del método Print anterior a Print(object value, bool prependTimestamp = false), por ejemplo, el método Print ya no sería compatible con Action<string> de acuerdo con las reglas de esta subcláusula.

nota final

21.5 Creación de instancias del delegado

Una instancia de un delegado se crea mediante un delegate_creation_expression (§12.8.17.5), una conversión a un tipo de delegado, combinación de delegados o eliminación de delegados. La instancia de delegado recién creada hace referencia a uno o varios de lo siguiente:

  • Método estático al que se hace referencia en la delegate_creation_expression o
  • Objeto de destino (que no puede ser null) y método de instancia al que se hace referencia en la delegate_creation_expression o
  • Otro delegado (§12.8.17.5).

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 dos instancias no nulldelegadas, sus listas de invocación se concatenan (primero el operando izquierdo y luego el operando derecho) para formar 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.17.5).

Los delegados se combinan con los operadores binarios + (§12.12.5) y += (§12.23.4). Un delegado se puede quitar de una combinación de delegados mediante los operadores binarios - (§12.12.6) y -= (§12.23.4). Los delegados se pueden comparar por igualdad (§12.14.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 instancian, cada uno encapsula un método. Cuando cd3 se instancia, tiene una lista de invocación de dos métodos, M1 y M2, en ese orden. La lista de invocación de cd4 contiene M1, M2 y M1, en ese orden. Para cd5, la lista de invocación contiene M1, M2, M1, M1 y M2, en ese orden.

Al crear un delegado a partir de otro delegado con una delegate_creation_expression, el resultado tiene una lista de invocación con una estructura diferente de la original, pero hace 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 M1 y M2, esos métodos se invocan mediante td3 en el mismo orden en que se invocan mediante cd3. Del mismo modo, cuando td4 se instancia, su lista de invocación tiene solo dos entradas, pero invoca los tres métodos, M1, M2 y M1, en ese orden, igual que cd4.

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

ejemplo final

Una vez instanciada, 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, se obtiene 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

21.6 Invocación de delegado

C# proporciona una sintaxis especial para invocar un delegado. Cuando se invoca una instancia de delegado que no es denull cuya lista de invocación contiene una entrada, invoca el método uno con los mismos argumentos que se dio y devuelve el mismo valor que el método al que se hace referencia. (Consulte §12.8.10.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 de forma sincrónica, en orden. A cada método se le pasa el mismo conjunto de argumentos que se le proporcionó a la instancia delegada. 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 inferiores de la lista de invocación. Si la invocación del delegado incluye parámetros de salida o un valor de devolución, 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 en la parte inferior de la lista de invocación.

Si se intenta invocar una instancia de delegado cuyo valor es null, se produce una excepción de tipo System.NullReferenceException.

Ejemplo: en el ejemplo siguiente se muestra cómo instanciar, 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 que se ha quitado realmente en su última aparición en la lista de invocación.

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