[C# Básico] Delegates

Hola a todos! Este es el tercer post de esa “serie” de C# Básico. En el primero vimos las interfaces y en segundo intenté responder a la pregunta de que es la herencia.

Hoy quiero hablaros de un tema sobre el que bastante gente tiene dificultades y sobre el que hay bastante confusión, pero que en el fondo es mucho más simple de lo que parece! Sí, me estoy refiriendo a los delegates (o delegados).

1. La necesidad de los delegates

Cuando queremos enviar valores a un método que son desconocidos para este, lo que hacemos es usar parámetros. Vamos, a nadie le sorprende el código:

int Sumar (int a, int b)
{
return a + b;
}

El método Sumar recibe dos parámetros y hace con ellos lo que sea (en este caso sumarlos). El uso de parámetros es vital porque sino deberíamos declarar un método por cada combinación posible de valores… En nuestro caso deberíamos hacer un método Sumar1y1 que sumase 1+1, otro Sumar1y2 que sumase 1+2…  ya se ve que eso por un lado es imposible y por otra un poco (bastante) estúpìdo. El uso de parámetros es algo que todos aprendemos más bien pronto al desarrollar y con los que todos nos sentimos cómodos.

Un parámetro tiene básicamente dos cosas:

  • Un nombre, que se usa dentro del método para referirse a él
  • Y un tipo que indica que clase de valores puede tener el parámetro.

El tipo es lo que me importa ahora: Si el parámetro es de tipo int puede contener cualquier entero, si es un double puede contener cualquier valor en coma flotante y si es FooClass puede contener cualquier objeto de la clase FooClass. Nada nuevo bajo el sol.

Bien, ahora imaginad que teneis una lista de cadenas y queréis ordenarla. Para ello os creais un método tal que:

void SortList(List<string> lst)
{
// ...
}

El método recibe un parámetro que es la lista a ordenar. Da igual que algoritmo de ordenación implementéis, en algún momento deberéis hacer algo como:

void SortList(List<string> lst)
{
// En str1 y str2 tenemos dos elementos que debemos comparar
if (FirstStringGoesBefore(str1, str2)) { ... }
}

El método FirstStringGoesBefore es un método helper vuestro que indica si una cadena va antes que otra según vuestro criterio de ordenación. P.ej. podría ser:

bool FirstString (string first, string second)
{
return first.Length <= second.Length;
}

Esto ordenaria las cadenas según su número de carácteres.

Vale… ¿observas un pequeño problema? ¿Cuál?

¡Exacto! Nuesta función SortList tiene el criterio de ordenación fijado, siempre ordena las cadenas según su número de caracteres! El algoritmo para ordenar cadenas (implementado en SortList) siempre es igual sea cual sea el criterio. Pero el criterio está en el método FirstStringGoesBefore() que decide, dadas dos cadenas, cual va antes que cual. No se a vosotros pero a mi, criterios de ordenación de cadenas me salen muchísimos: por número de carácteres, alfabéticamente, según representación numérica (es decir “01” antes que “112” porque 1 es menor que 112), etc, etc

La pregunta es debo hacer un método SortList para cada criterio? Es decir debo hacer un SortListByLength, otro SortListByAlphabetic,…

Dicho de otro modo: no le podríamos pasar al método SortList el criterio a utilizar como un parámetro? Ya, pero resulta que el criterio de ordenación resulta que es un método, no un valor (como int) o un objeto. Pues bien si comprendes la necesidad de pasar métodos como parámetros, comprendes la necesidad de los delegates. Y es que un delegate no es nada más que la posibilidad de pasar un método como parámetro.

2. Declración de un delegate

Cuando declaramos un parámetro, el tipo de ese parámetro determina que valores son válidos. Si el parámetro és int el valor 2 es correcto pero el valor 3.14159 no, ya que cae fuera de los valores válidos para int. Si el parámetro es FooClass entonces cualquier objeto FooClass es válido, pero un objeto de cualquier otra clase (p.ej. string) pues no.

Los delegates sirven para poder pasar métodos como parámetros y también hay varios tipos de delegates. Tiene lógica: no es lo mismo un método que espera dos cadenas y devuelve un bool que otra que espera dos ints y no devuelve nada. El primero sería un criterio de ordenación válido, la segunda no. Si yo creo el método SortList y admito un delegate como parámetro para el criterio de ordenación, es lógico que quiera asegurarme que sólo funciones que aceptan dos strings y devuelven bool són válidas. Total: yo voy a usar ese delegate pasándole dos cadenas y esperando un bool.

Por ello, cuando se define un delegate debe definirse que métodos acepta ese delegate: o sea los parámetros y el valor de retorno que los métodos deben tener.

Así no es de extrañar que la definición de un delegate se parezca sospechosamente a la definición de un método:

delegate bool CriterioOrdenacion(string str1, string str2);

Fijaos en el uso de la palabra clave delegate, que indica que lo que viene a continuación es la declaración de un delegate (y no de un método). El ejemplo declara un delegate llamado CriterioOrdenacion que permite pasar como parámetro métodos que acepten dos cadenas y que devuelva bool.

Nota: Que las dos cadenas se llamen str1 y str2 es totalmente superficial y no tiene ninguna importancia. Si yo creo un método que acepte dos cadenas y devuelva un bool podré pasarlo como parámetro usando este delegate aunque las cadenas no se llamen str1 y str2. Personalmente creo que el hecho de que en la declaración de un delegate los parámetros aparezcan nombrados sólo crea confusión.

3. Uso de un delegate

Una vez tengo definido el delegate ya puedo usarlo. Cuando defino un delegate creo un nuevo tipo de datos. Así pues la el método SortList quedaría como:

void SortList(List<string> lst, CriterioOrdenacion criterio)
{
// ...
if (criterio(first, second)) {...}
}

Fijaos en dos cosas:

  1. El parámetro criterio cuyo tipo es CriterioOrdenacion que es el delegate que habíamos definido antes.
  2. La “invocación” al delegate. Para invocar a un delegate se invoca como si fuese un método. Es decir criterio(first, second) llamará realmente al método que hayamos recibido como parámetro. Nosotros no sabemos cual es ese método, pero gracias al delegate
    1. Sabemos que recibe dos cadenas y devuelve un bool
    2. Podemos invocarlo

Bien, ahora nos falta ver como podemos llamar al método SortList pasándole un criterio de ordenación en concreto. Para ello hemos de ver como instanciar un delegate.

Imaginad que tengo un método llamado CriterioOrdenacionSegunLength que queremos usar como un criterio de ordenación:

bool CriterioOrdenacionSegunLength(string s1, string s2)
{
return s1.Length <= s2.Length;
}

Y ahora quiero invocar al método SortList usando el método CriterioOrdenacionSegunLength como criterio de ordenación. Para ello defino una variable de tipo CriterioOrdenacion (el delegate) y la instancío con el método:

CriterioOrdenacion miCriterio = 
new CriterioOrdenacion(CriterioOrdenacionSegunLength);

Para instanciar un delegate se usa new al igual que para instanciar una clase, y se pasa el nombre del método con el que se instancia este delegate.

Y ahora ya puedo llamar a SortList:

SortList(lst, miCriterio);

Y listos! El método SortList usará ahora el criterio de ordenación del delegate miCriterio que es el método CriterioOrdenacionSegunLength.

4. Delegates y eventos

Mucha gente confunde delegates y eventos, aunque no es exactamente lo mismo. Ya hemos visto un delegate, y lo que conocemos como “evento” se basa, ciertamente, en delegates. Lo que pasa es que podemos ver un evento como una lista de delegates (lo que en .NET llamamos un MulticastDelegate). Cuando en C# estamos haciendo:

btn.Click += new EventHandler(btn_Click);

Estáis creando un delegate de tipo EventHandler y lo añadís a “la lista” de delegates del evento. Cuando el botón lanza el evento, lo que hace es invocar uno a uno todos los delegates de la lista, y así es como se llama al método btn_Click. Si mirais como está definido EventHandler vereis que su definición es:

public delegate void EventHandler(object sender, EventArgs e);

De ahí los famosos dos parámetros que tenemos en (casi) todas nuestras funciones gestoras de eventos!

5. Delegates y threads

Hay gente que también confunde delegates con threads. Delegates ya hemos visto que son, y los threads no son nada más que ejecutar un código en segundo plano. La confusión viene porque para crear un thread debemos decirle cual es el código a ejecutar en segundo plano. Es decir que método ejecutar en segundo plano. Es decir, debemos pasarle un delegate.

6. Para ver más…

Me he dejado cosas en tintero, porque sino este post, honestamente, no se terminaría nunca… Si os interesa profundizar sobre el tema, sabed que me he dejado adrede (os dejo algunos links):

  1. Delegates y genéricos
  2. Delegates y métodos anónimos
  3. Lambda expressions
  4. Delegates y reflection
  5. Reglas de covarianza y contravarianza de los delegates

Si alguien está interesado en profundizar sobre alguno de esos temas, que me lo diga y veremos que podemos hacer al respecto! 😉

Espero que este post os haya ayudado a tener más claro el concepto de un delegate!

Un saludo! 😉

15 comentarios en “[C# Básico] Delegates”

  1. Que buen post, todos estos temas los explicas muy bien, para seguir podriamos hablar de las nuevas caracteristicas de c# 4.0 me parece muy bacano hablar de los tipos dinamicos.

  2. Si así lo explicarán en la escuela no tendriamos que agarrarnos a portazos para enteder el tema.

    Enhorabuena por más de estos artículos, maestro!

    Saludos,

  3. Coom siemrep excelente… Yo no ententía los Delegados, hasta ahora. De verdad muchas gracias.
    Super el tema de los Lambda que para mi es toro tema confuso…
    Saludos desde Colombia!

  4. La primera parte del artículo separando la habitual función de delegados como acompañante de eventos me ha parecido muy acertada.

  5. Excelente post!!

    Hacia tiempo que se me atragantaban los delegates, pero con esta explicación han quedado bastante claros.

    Muchas gracias!!!

    Un saludo,
    José Luis.

  6. Por fin!, ahora entiendo el concepto de delegado, más bien, ahora entiendo qué uso le puedo dar. Felicitaciones maestro, muy buen artículo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *