Utilizar delegados (Guía de programación de C#)

Un delegado es un tipo que encapsula de forma segura un método, similar a un puntero de función en C y C++. A diferencia de los punteros de función de C, los delegados están orientados a objetos, proporcionan seguridad de tipos y son seguros. El tipo de un delegado se define por el nombre del delegado. En el ejemplo siguiente, se declara un delegado denominado Callback que puede encapsular un método que toma una string como argumento y devuelve void:

public delegate void Callback(string message);

Normalmente, un objeto delegado se construye al proporcionar el nombre del método que el delegado encapsulará o con una expresión lambda. Una vez que se crea una instancia de un delegado de esta manera, se puede invocar. Al invocar un delegado, se llama al método asociado a la instancia del delegado. Los parámetros pasados al delegado por el autor de la llamada se pasan a su vez al método, y el valor devuelto desde el método, si lo hubiera, es devuelto por el delegado al autor de la llamada. Por ejemplo:

// Create a method for a delegate.
public static void DelegateMethod(string message)
{
    Console.WriteLine(message);
}
// Instantiate the delegate.
Callback handler = DelegateMethod;

// Call the delegate.
handler("Hello World");

Los tipos de delegado se derivan de la clase Delegate en .NET. Los tipos de delegados son sealed (no se pueden derivar) y no se pueden derivar clases personalizadas de Delegate. Dado que el delegado con instancias es un objeto, puede pasarse como argumento o asignarse a una propiedad. De este modo, un método puede aceptar un delegado como parámetro y llamar al delegado en algún momento posterior. Esto se conoce como devolución de llamada asincrónica y es un método común para notificar a un llamador que un proceso largo ha finalizado. Cuando se utiliza un delegado de esta manera, el código que usa al delegado no necesita ningún conocimiento de la implementación del método empleado. La funcionalidad es similar a la encapsulación que proporcionan las interfaces.

Otro uso común de devoluciones de llamada es definir un método de comparación personalizado y pasar ese delegado a un método de ordenación. Permite que el código del llamador se convierta en parte del algoritmo de ordenación. En el siguiente método de ejemplo se usa el tipo Del como parámetro:

public static void MethodWithCallback(int param1, int param2, Callback callback)
{
    callback("The number is: " + (param1 + param2).ToString());
}

Luego puede pasar el delegado creado anteriormente a ese método:

MethodWithCallback(1, 2, handler);

Y recibir los siguientes resultados en la consola:

The number is: 3

Si se usa el delegado como abstracción, no es necesario que MethodWithCallback llame directamente a la consola —no tiene que estar diseñado pensando en una consola—. Lo que MethodWithCallback hace es simplemente preparar una cadena y pasarla a otro método. Esto es especialmente eficaz, puesto que un método delegado puede utilizar cualquier número de parámetros.

Cuando se crea un delegado para encapsular un método de instancia, el delegado hace referencia tanto a la instancia como al método. Un delegado no tiene conocimiento del tipo de instancia —aparte del método al que encapsula—, por lo que un delegado puede hacer referencia a cualquier tipo de objeto siempre que haya un método en ese objeto que coincida con la signatura del delegado. Cuando se crea un delegado para encapsular un método estático, solo hace referencia al método. Considere las siguientes declaraciones:

public class MethodClass
{
    public void Method1(string message) { }
    public void Method2(string message) { }
}

Junto con el DelegateMethod estático mostrado anteriormente, ahora tenemos tres métodos que se pueden encapsularse mediante una instancia de Del.

Un delegado puede llamar a más de un método cuando se invoca. Esto se conoce como multidifusión. Para agregar un método adicional a la lista de métodos del delegado —la lista de invocación—, simplemente es necesario agregar dos delegados mediante los operadores de adición o asignación y suma ('+' o '+='). Por ejemplo:

var obj = new MethodClass();
Callback d1 = obj.Method1;
Callback d2 = obj.Method2;
Callback d3 = DelegateMethod;

//Both types of assignment are valid.
Callback allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;

En este momento allMethodsDelegate contiene tres métodos en su lista de invocación: Method1, Method2 y DelegateMethod. Los tres delegados originales, d1, d2 y d3, permanecen sin cambios. Cuando se invoca allMethodsDelegate, todos los métodos se llaman en orden. Si el delegado usa parámetros de referencia, la referencia se pasa secuencialmente a cada uno de los tres métodos por turnos, y cualquier cambio que realice un método es visible para el siguiente método. Cuando alguno de los métodos produce una excepción que no se captura dentro del método, esa excepción se pasa al llamador del delegado y no se llama a los métodos siguientes de la lista de invocación. Si el delegado tiene un valor devuelto o los parámetros de salida, devuelve el valor devuelto y los parámetros del último método invocado. Para quitar un método de la lista de invocación, utilice los operadores de decremento o de asignación de decremento (- o -=). Por ejemplo:

//remove Method1
allMethodsDelegate -= d1;

// copy AllMethodsDelegate while removing d2
Callback oneMethodDelegate = allMethodsDelegate - d2;

Dado que los tipos de delegado se derivan de System.Delegate, los métodos y propiedades definidas por esa clase se pueden llamar en el delegado. Por ejemplo, para buscar el número de métodos en la lista de invocación de un delegado, se puede escribir:

int invocationCount = d1.GetInvocationList().GetLength(0);

Los delegados con más de un método en su lista de invocación derivan de MulticastDelegate, que es una subclase de System.Delegate. El código anterior funciona en ambos casos porque las dos clases admiten GetInvocationList.

Los delegados de multidifusión se utilizan mucho en el control de eventos. Los objetos de origen de evento envían notificaciones de evento a los objetos de destinatario que se han registrado para recibir ese evento. Para suscribirse a un evento, el destinatario crea un método diseñado para controlar el evento; a continuación, crea a un delegado para dicho método y pasa el delegado al origen de eventos. El origen llama al delegado cuando se produce el evento. Después, el delegado llama al método que controla los eventos en el destinatario y entrega los datos del evento. El origen del evento define el tipo de delegado para un evento determinado. Para obtener más información, consulte Eventos.

La comparación de delegados de dos tipos distintos asignados en tiempo de compilación generará un error de compilación. Si las instancias de delegado son estáticamente del tipo System.Delegate, entonces se permite la comparación, pero devolverá false en tiempo de ejecución. Por ejemplo:

delegate void Callback1();
delegate void Callback2();

static void method(Callback1 d, Callback2 e, System.Delegate f)
{
    // Compile-time error.
    //Console.WriteLine(d == e);

    // OK at compile-time. False if the run-time type of f
    // is not the same as that of d.
    Console.WriteLine(d == f);
}

Consulte también