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

Actualización: noviembre 2007

Un delegado es un tipo de objeto que encapsula un método de forma segura, similar a un puntero a función de C y C++. A diferencia de los punteros a 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 su nombre. El ejemplo siguiente declara un delegado denominado Del que puede encapsular un método que toma un valor string como argumento y devuelve un valor void:

public delegate void Del(string message);

Generalmente, un objeto de delegado se crea proporcionando el nombre del método que el delegado contendrá o con un método anónimo. Una vez que se crean instancias de un delegado, el delegado pasará al método una llamada realizada por éste al delegado. Los parámetros que el llamador pasó al delegado se pasan al método y el delegado devuelve al llamador el valor devuelto del método, si hay alguno. Esto se conoce como invocar al delegado. Se puede invocar un delegado con instancias como si fuera el propio método contenido. Por ejemplo:

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

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

Los tipos de delegados se derivan de la clase Delegate en .NET Framework. Los tipos de delegados son sealed (no se pueden derivar) y no es posible derivar clases personalizadas de Delegate. Puesto que el delegado con instancias es un objeto, puede pasarse como parámetro o asignarse a una propiedad. Esto permite que un método acepte un delegado como parámetro y llame al delegado posteriormente. Esto se conoce como devolución de llamada asincrónica y constituye un método común de notificación de un llamador cuando ha finalizado un proceso largo. Cuando se utiliza un delegado de esta forma, no es necesario que el código que utiliza el delegado conozca la implementación del método que se está utilizando. La funcionalidad es similar a la encapsulación que proporcionan las interfaces. Para obtener más información, vea Cuándo se utilizan delegados en lugar de interfaces (Guía de programación de C#).

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. Esto permite al código del llamador ser parte del algoritmo de ordenación. El método del ejemplo siguiente utiliza el tipo Del como parámetro:

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

A continuación, se puede pasar el delegado creado anteriormente a ese método:

MethodWithCallback(1, 2, handler);

y recibir el resultado siguiente en la consola:

The number is: 3

Al utilizar el delegado como abstracción, MethodWithCallback no es necesario llamar a la consola directamente; es decir, el delegado no se tiene que diseñar 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 contener un método de instancia, el delegado hace referencia tanto a la instancia como al método. Un delegado no conoce el tipo de instancia a parte del método que éste contiene, de modo que un delegado puede hacer referencia a cualquier tipo de objeto siempre que exista un método en dicho objeto que coincida con la firma del delegado. Cuando se crea un delegado para contener un método estático, éste sólo 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 método estático DelegateMethod que se mostró previamente, ahora tenemos tres métodos que la instancia de Del puede contener.

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

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

//Both types of assignment are valid.
Del 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 no cambian. Cuando se invoca a allMethodsDelegate, se llama a los tres métodos por orden. Si el delegado utiliza parámetros de referencia, ésta a su vez se pasa secuencialmente a cada uno de los tres métodos y todos los cambios efectuados por un método son visibles para el siguiente método. Cuando alguno de los métodos produce una excepción que no se detecta dentro del método, esa excepción se pasa al llamador del delegado y no se llama a ninguno de los métodos siguientes de la lista de invocación. Si el delegado tiene un valor devuelto y/o fuera de los parámetros, 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 el operador de resta o de asignación de resta ('-' o '-='). Por ejemplo:

//remove Method1
allMethodsDelegate -= d1;

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

Puesto que los tipos de delegados se derivan de System.Delegate, los métodos y las propiedades definidos 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, 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 ampliamente en el control de eventos. Los objetos de origen de eventos envían notificaciones de eventos a objetos de destinatario registrados para recibir ese evento. Para registrar un evento, el destinatario crea un método diseñado para controlar el evento, a continuación crea un delegado para dicho método y pasa al delegado al origen de eventos. El origen llama al delegado cuando se produce el evento. Luego el delegado llama al método de control de eventos del destinatario y entrega los datos del evento. El origen de eventos define el tipo de delegado para un evento dado. Para obtener más información, vea Eventos (Guía de programación de C#).

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

delegate void Delegate1();
delegate void Delegate2();

static void method(Delegate1 d, Delegate2 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.
    System.Console.WriteLine(d == f);
}

Vea también

Conceptos

Guía de programación de C#

Referencia

Delegados (Guía de programación de C#)

Covarianza y contravarianza en los delegados (Guía de programación de C#)

Eventos (Guía de programación de C#)