martes, 2 de agosto de 2011

Delegados y expresiones lambda en VB.NET (II)

Qué era un delegado y para que servía lo vimos en el post Delegados en VB.NET (I).

En este otro post lo que veremos serán los delegados genéricos Action y Func.
Si esto te suena a chino, sigue leyendo que prometo explicarte su significado.

No hace mucho hablamos sobre los genéricos. Hablamos sobre colecciones genéricas y sobre métodos y clases genéricas. Por supuesto y según vamos adentrándonos en el tema de los genéricos, vemos que son una característica de los lenguajes .NET que ha aterrizado con fuerza y que cubre casi todos los escenarios.

Siendo así, los delegados no podían quedarse fuera del mundo “genérico” y es por ello que nos encontramos con Action y Func.

Action y Func son 2 delegados genéricos de propósito general que permiten al desarrollador ahorrar código cuando trabaja con delegados (además de hacer posible que la magia de LINQ funcione, pero ese es otro tema…).

Action representa a un método (Sub en VB.NET o función que retorna void en C#) que acepta desde 0 a 16 argumentos, mientras que Func representa a una función que igualmente acepta desde 0 a 16 argumentos, pero además devuelve un valor.

De este modo, si alguna vez vas a escribir un delegado que encaje en la firma de alguno de estos 2 delegados genéricos (sino encaja tienes un problema porque tu método o función tiene más de 16 parámetros y deberías hacértelo mirar)… no lo escribas y utiliza los delegados genéricos. Esa es su función, ahorrarte escribir código cuando trabajas con delegados.

Primero veamos los prototipos de Action y Func.

clip_image001

clip_image002

A propósito de todos estos métodos, hay un nuevo concepto que salta a la palestra y es la covarianza y contravarianza (o covariante y contravariante) en genéricos.

Puedes ver el enlace en MSDN que habla sobre estos conceptos.

Aunque no me quiero liar con esto, es un tema importante que bien merece una explicación (aunque probablemente ni yo mismo me queda satisfecho con ella).

Lo primero que hay que hacer es reconocer si los parámetros o valor devuelto por estos delegados genéricos son covarianza o contravarianza.

·         La covarianza se indica con la palabra clave Out.

·         La contravarianza con la palabra clave In.

Si ahora cogemos un delegado de tipo Action y un delegado de tipo Func:

Public Delegate Sub Action(Of In T1, In T2)(arg1 As T1, arg2 As T2)

Public Delegate Function Func(Of In T1, In T2, Out TResult)(arg1 As T1, arg2 As T2) As TResult

Podemos ver que:

  • Todos los parámetros tanto de Action como de Func, son In, luego son contravarianza.
  • El valor devuelto por Func es Out, luego covarianza.

Por cierto, si te crees que todo esto me lo estoy inventado, echa un vistazo al examinador de objetos y mira cómo se habla sobre esto sin ningún tipo de pudor.

clip_image004

OK, perfecto, pero ¿Qué es covarianza y contravarianza?

La covarianza (Out) dice que donde espero recuperar un tipo puede recuperar un tipo derivado o descendiente (digo recuperar porque Out sólo se utiliza como valor de retorno del delegado genérico Func, de hecho por eso se llama Out, porque es de Salida). Por ejemplo, donde espero recuperar un Coche también podré recuperar un Deportivo (porque un Deportivo es un tipo de Coche). Lo cierto es que es la covarianza es intuitiva y no difiere mucho del polimorfismo al que estamos acostumbrados a día de hoy.

Sin embargo la contravarianza dice todo lo contrario, donde espero recibir (ahora estamos hablando de parámetros que reciben valores tanto en Action como en Func, por eso se llama In) un Deportivo también podré recibir un Coche. Es decir, la contravarianza permite la asignación de ancestros pero no de descendientes.

Tengo que reconocer que ni yo mismo y a día de hoy tengo muy claro cuando querré utilizar un ancestro (con contravarianza), pero imagino que todo llegará.

En cualquier caso, puedes visitar en siguiente enlace donde se explica este concepto mejor que aquí ;-)

Pasado ya el mal trago de covarianza y contravarianza (te juro que me ha costado un par de neuronas escribir los anteriores párrafos), es hora de volver con nuestros delegados genéricos (ahora ya sabiendo leer su firma por completo, incluidas las “misteriosas” palabras In y Out).

Vamos a tomar el siguiente ejemplo sin delegados genéricos, para luego pasarlo a delegados genéricos, y así poder ver los cambios:

Module Module1

 

    Sub Main()

        Dim calc As New Calculadora

        calc.Sumar(5, 5, AddressOf Sumar)

    End Sub

 

    Public Function Sumar(ByVal p1 As Integer, ByVal p2 As Integer) As Integer

        Return p1 + p2

    End Function

 

End Module

 

Public Class Calculadora

 

    Public Delegate Function OperacionDeSuma(ByVal p1 As Integer, ByVal p2 As Integer) As Integer

 

    Public Function Sumar(ByVal p1 As Integer, ByVal p2 As Integer, ByVal operacion As OperacionDeSuma) As Integer

        Return operacion(p1, p2)

    End Function

 

End Class


Ahora con genéricos:

Module Module1

 

    Sub Main()

        Dim calc As New Calculadora

        calc.Sumar(5, 5, AddressOf Sumar)

    End Sub

 

    Public Function Sumar(ByVal p1 As Integer, ByVal p2 As Integer) As Integer

        Return p1 + p2

    End Function

 

End Module

 

Public Class Calculadora

 

    Public Function Sumar(ByVal p1 As Integer, ByVal p2 As Integer, ByVal operacion As Func(Of Integer, Integer, Integer)) As Integer

        Return operacion(p1, p2)

    End Function

 

End Class

 
En este código hay que fijarse que ahora ya no hemos tenido que declara un delegado, sino que hemos especificado que esperamos recibir un delegado genérico de tipo Func con 2 parámetros de tipo Integer y devolviendo un valor de tipo Integer.

En cualquier caso, tampoco parece que el ahorro de código haya sido muy grande (sólo hemos suprimido la línea que declaraba el delegado). Esto es porque los delegados genéricos vienen de la mano con otra nueva característica de .NET, las expresiones lambda (aquí quería llegar yo), y esto sí supondrá un ahorro de código importante.

Los detractores de los delegados genéricos podrán decir que decía mucho más sobre la operación solicitada un delegado llamado OperacionDeSuma que un Func (¿Func de qué?) Esto es lo que se pierde con los delegados genéricos, el nombre descriptivo de lo que estamos pidiendo.

Por ejemplo, el anterior código sería equivalente a este otro:

Module Module1

 

    Sub Main()

        Dim calc As New Calculadora

 

        ' Opción 1

        ' Declaramos la función lambda y la guardamos en una variable

        Dim miExpresion = Function(p1, p2) p1 + p2

        ' Se pasa la expresión lambda donde se esperaba un delegado

        calc.Sumar(5, 5, miExpresion)

 

        ' Opción 2, la forma preferida.

        ' De esta otra forma, directamente escribimos la expresión lambda

        calc.Sumar(5, 5, Function(p1, p2) p1 + p2)

    End Sub

 

End Module

 

Public Class Calculadora

 

    Public Function Sumar(ByVal p1 As Integer, ByVal p2 As Integer, ByVal operacion As Func(Of Integer, Integer, Integer)) As Integer

        Return operacion(p1, p2)

    End Function

 

End Class


Como vemos, ahora ya tampoco hemos tenido que definir un método externo para pasar como parámetro a la función Sumar. En vez de eso, hemos escrito el código que implementa nuestra Suma en línea. ¿Esto ya es otra cosa, no? Aquí si estamos escribiendo menos código.

Function(p1, p2) p1 + p2 es una expresión lambda.

Una expresión lambda es una función o procedimiento sin nombre que sólo puede tener una instrucción. En cualquier sitio donde nos pidan un delegado, podemos utilizar una expresión lambda en sustitución. De esta forma, una expresión lambda es una forma más compacta de pasar métodos como parámetro a otros métodos.

En otros lenguajes también se las llama funciones en línea o funciones anónimas.

Fijarse que he dicho que una expresión lambda es un procedimiento o función.
Es decir, el siguiente código también es válido:

    Public Sub OtraSuma(ByVal p1 As Integer, ByVal p2 As Integer, ByVal operacion As Action(Of Integer, Integer))

        operacion(p1, p2)

    End Sub

 

        Dim calc As New Calculadora

        calc.OtraSuma(5, 5, Sub(p1, p2) Console.WriteLine(p1 + p2))

 

Veamos el anterior código en C# (lo digo porque cuando busquemos información sobre lambda, casi siempre el código será C# y tenemos que saber leerlo).

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            var calc = new Calculadora();

            // En C# la sintaxis de una expresión lambda es argumentos=>instrucción

            calc.Sumar(5, 5, (p1, p2) => p1 + p2);

        }

    }

}

 

public class Calculadora

{

    public int Sumar(int p1, int p2, Func<int, int, int> operacion)

    {

        return operacion(p1, p2);

    }

}


Lo importante de este código C# es la sintaxis argumentos => instrucción.
Lo cierto es que llevaba mucho tiempo leyendo esta sintaxis pero hasta hoy no la he entendido. ¿Por qué narices no aposté por C# cuando me pase a .NET?

Por otro lado, si nos fijamos podemos comprobar que las expresiones lambda no necesitan especificar el tipo de sus parámetros puesto que el tipo de cada uno de ellos se infiere de la definición del delegado sobre el que trabajan.

Lo cierto es que hemos empezado la casa por el tejado, pero quería ver funcionar una expresión lambda antes de continuar hablando sobre ella. Ahora sí podemos ver cuáles son los requerimientos de una expresión lambda.

  • Sólo pueden contener una instrucción (pero varias líneas si separamos la instrucción con el guión bajo). En cualquier caso, esa única instrucción podría ser una llamada a otra función (lambda o no) y así no tendríamos que estar atados a la limitación de una sola línea de código.
  • Normalmente no se especificarán el tipo de los parámetros de la función porque se infieren. Digo “normalmente” porque se podría crear directamente una expresión lambda como la siguiente, y aquí sí sería necesario especificar el tipo de los parámetros para que, internamente, el compilador cree por nosotros un delegado genérico del tipo 

    Public Delegate Function Func(Of In T1, In T2, Out TResult)(arg1 As T1, arg2 As T2) As TResult

    Dim otraExpresion = Function(p1 As Integer, p2 As Integer) p1 + p2

    Console.WriteLine(otraExpresion(5, 5))

  • No se especificará el tipo devuelto ni se utilizará la palabra Return. De nuevo el valor de retorno se infiere.
  • La función no tendrá nombre ni tampoco se especificará un fin con End Function.
  • El ámbito de una función lambda es todo lo que puede ver el método que la contiene. Es decir, por defecto es como una función normal pero además también puede acceder a las variables locales definidas en el método donde se declara la expresión (no pensemos en la expresiones lambda como “islitas” alejadas del mundo exterior).
  • Si se quiere (porque se puede) especificar el tipo de los parámetros, tienen que coincidir con los del delegad, y además si especifica un tipo de un parámetro, ya será obligatorio también para el resto de parámetros.
  • No se puede utilizar parámetros opcionales (Optional), ParamArray ni parámetros genéricos.

Vale, yo creo que aquí (y si has seguido leyendo y no te has cansado ya), ya sabemos lo que es una expresión lambda pero ¿Por qué son tan importantes?

Pues básicamente por una cosa… LINQ. Yo creo que todo LINQ debe ser una super expresión lambda… es coña, pero lo cierto es que se utiliza muchísimo, sobre todo como parte de los árboles de expresión (aquí ya me estaría columpiando si hablo más). Por otro lado, también ASP.NET MVC y los Helpers utilizan de forma masiva las expresiones lambda, así que… Bienvenidas Expresiones Lambda!

Un saludo y… basta por hoy!

1 comentario:

  1. Tremendo Post mi hermano , e desarrollado varios proyects , pero en ado.net y uno que otro en entity, Los delegados me kedo muy claro

    ResponderEliminar