Para los que usan LINQ, las expresiones lambda son algo conocido ya que la mayor parte de los métodos de ese lenguaje usan delegados para permitirnos ordenar, filtrar, proyectar y realizar acciones sobre colecciones de objetos.
Pero… ¿qué es una expresión lambda?. Bueno, pues una Expresión Lambda es una función o subrutina con las siguientes características:
- No tiene nombre o sea, es anónima
- Puede contener una sola línea o múltiples líneas
- No tiene modificadores de acceso (Public, Private, Overloads, etc.)
- Su tipo de retorno es inferido, y
- Se puede usar en cualquier lugar donde se permita usar un delegado, por ejemplo, en los métodos Find o FindAll de las listas genéricas.
Las expresiones lambda se incluyeron a partir de la versión 3.0 del .Net Framework como una especie de sustitución de los métodos anónimos que se podían usar con C# desde la versión 2.0 de dicho framework y que nunca estuvieron disponibles para VB y que servían para facilitar la declaración “inline” de un método usando la palabra reservada delegate.
Como las expresiones lambda son una manera más corta y concisa de escribir delegados, se pueden usar en cualquier parte donde los delegados sean admitidos así que, al igual que éstos, también pueden ser pasadas como argumentos a funciones y procedimientos.
Una expresión lambda puede ser asignada a una variable de la misma manera que se puede hacer con un delegado para usarse posteriormente, por ejemplo:
- Dim Suma = Function(numero1 As Integer, numero2 As Integer) numero1 + numero2
En la asignación del ejemplo, se puede ver que la expresión lambda se asigna a una variable, la expresión lambda cumple con todas las características mencionadas al principio de este artículo, como que la función no tiene nombre, ejecuta una sola línea de código, no hay modificadores de acceso y no se especifica el tipo de dato de retorno del resultado.
Cabe mencionar que la expresión se pudo haber escrito en mas líneas si hubiera querido, por ejemplo:
- Dim Suma = Function(numero1 As Integer, numero2 As Integer)
- Return numero1 + numero2
- End Function
Solo que hay que colocar un End Function o un End Sub según se trate. En el caso de C# bastaría con encerrar las instrucciones de la expresión lambda entre llaves { } que son los especificadores de bloques de código de ese lenguaje de programación.
Ahora, para usar esa expresión lambda asignada a la variable “Suma”, bastaría hacer algo como lo siguiente para mostrar un número 3 en la consola de salida:
- Console.WriteLine(Suma(1, 2))
Pero se pueden preguntar ¿y que ventaja tienen entonces las expresiones lambda? eso también se puede hacer con una función común y corriente sin necesidad de usar una variable y también se puede hacer con un delegado. Pero una de las ventajas de usar expresiones lambda es que permiten simplificar el código y hacerlo todo como decimos en México “de un jalón” de la siguiente manera:
- Console.WriteLine((Function(numero1 As Integer, numero2 As Integer) numero1 + numero2)(1, 2))
Incluso, se puede omitir el tipo de datos de los parámetros que recibe la función, quedando como se ve a continuación:
- Console.WriteLine((Function(numero1, numero2) numero1 + numero2)(1, 2))
Y si aún así creen que esto no tiene nada de sorprendente ni de útil. Bueno, pues entonces vamos a ver un ejemplo que hará algo más complicado con la intención de esclarecer la utilidad y la conveniencia de las expresiones lambda.
Imaginemos tener una clase Empleado definida de la siguiente forma:
- Public Class Empleado
- Public Property Nombre As String
- Public Property ApellidoPaterno As String
- Public Property ApellidoMaterno As String
- Public Property Edad As Byte
- Public Property Salario As Decimal
- End Class
Y que cargamos una lista con objetos de este tipo, por ejemplo:
- Sub CargaLista(ByVal lista As List(Of Empleado))
- lista.Add(New Empleado() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías", .Edad = 30, .Salario = 15000.0})
- lista.Add(New Empleado() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo", .Edad = 42, .Salario = 16000.0})
- lista.Add(New Empleado() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 50, .Salario = 10000.0})
- lista.Add(New Empleado() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro", .Edad = 45, .Salario = 8000.0})
- lista.Add(New Empleado() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López", .Edad = 37, .Salario = 12000.0})
- End Sub
Si tuviéramos que hacer búsquedas en esta lista utilizando por ejemplo el método Find de la misma tendríamos que crear un delegado de tipo Predicate para identificar el elemento que coincide con la condición de búsqueda. He aquí una muestra:
- Private Function BuscaPorSalario(ByVal e As Empleado) As Boolean
- If e.Salario = varSalario Then
- Return True
- Else
- Return False
- End If
- End Function
Y en el método Find pasar la dirección del Predicate como sigue:
- lista.Find(AddressOf BuscaPorSalario)
Al pasar la dirección de memoria al método Find de la lista, el compilador crea un bucle interno en el código IL para utilizar la función de delegado BuscaPorSalario con cada elemento de la lista para ver si la condición de búsqueda se cumple. Pero si quisiéramos buscar por medio de otra propiedad por ejemplo, su apellido paterno o su edad, tendríamos que escribir otro Predicate que identificara el elemento de acuerdo a esa otra propiedad, lo cual es una solución pero no es la ideal. En cambio, usando expresiones lambda, la tarea se simplifica muchísimo. Para demostrarlo, podemos hacer un método de prueba donde se verifica el resultado de algunas búsquedas en la lista, efectuadas utilizando expresiones lambda que usan diferentes propiedades de los elementos como a continuación se muestra:
- <TestMethod()>
- Sub Prueba()
- Dim lista As List(Of Empleado) = New List(Of Empleado)
- Dim Salario As Decimal = 10000D
- CargaLista(lista)
- Assert.AreEqual("José", (lista.Find(Function(e) e.Salario = Salario)).Nombre)
- Assert.AreEqual("Noé", (lista.Find(Function(e) e.ApellidoPaterno = "García")).Nombre)
- Assert.AreEqual("Enrique", (lista.Find(Function(e) e.Edad = 42)).Nombre)
- End Sub
Como se puede ver en las líneas 8, 9 y 10 del método de prueba, se verifican los resultados de las búsquedas en la lista de objetos empleado pero utilizando expresiones lambda para facilitar la búsqueda de dichos elementos en base a distintas propiedades de los mismos, reduciendo la cantidad de código y facilitando la lectura del mismo, además, si se mira con atención la línea 8, se puede ver como la expresión lambda utiliza la variable Salario declarada e inicializada en la línea 4 para identificar el elemento que cumple con la condición de búsqueda. Esto es posible con las expresiones lambda pero no con los delegados de tipo predicate, ya que las expresiones lambda tienen acceso a cualquier variable local y global visible en el contexto donde se encuentre escrita. En el caso de usar un predicate, la variable Salario tendría que ser visible a nivel de clase, formulario o global.
Ahora para finalizar, demostraré la utilidad de las expresiones lambda retomando el ejemplo de una entrada anterior donde hablaba sobre los delegados (a la que los remito si aún no la han leído) donde se terminó con un procedimiento genérico que implementaba el algoritmo de ordenamiento de burbuja que muestro a continuación:
- Public Sub Sort(Of T)(ByVal Lista As IList(Of T), ByVal Comparador As ComparerFunction(Of T))
- Dim HuboCambio As Boolean
- Do
- HuboCambio = False
- For i As Integer = 0 To Lista.Count - 1
- If Comparador(Lista(i), Lista(i + 1)) > 0 Then
- Dim oTemp As T = Lista(i)
- Lista(i) = Lista(i + 1)
- Lista(i + 1) = oTemp
- HuboCambio = True
- End If
- Next
- Loop While (HuboCambio)
- End Sub
donde se puede ver que se utiliza un delegado genérico para realizar la comparación entre los elementos de una lista. Por lo que, muestro también la declaración del delegado genérico:
- Public Delegate Function ComparerFunction(Of T)(ByVal a As T, ByVal b As T) As Integer
y del método compatible con el delegado genérico:
- Private Function ComparePersonByApellidos(ByVal a As Persona, ByVal b As Persona) As Integer
- Return String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno))
- End Function
Y también un mostraré el método de prueba de estos pequeños trozos de código:
- Imports System.Text
- Imports DelegatesAndLambdas
- <TestClass()>
- Public Class BubbleSorterTest
- <TestMethod()>
- Public Sub CreateAndSortAList()
- Dim lista As List(Of Persona) = New List(Of Persona)
- lista.Add(New Persona() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías"})
- lista.Add(New Persona() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo"})
- lista.Add(New Persona() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
- lista.Add(New Persona() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro"})
- lista.Add(New Persona() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
- Dim callback As ComparerFunction(Of Persona) = New ComparerFunction(Of Persona)(AddressOf CompareByApellidos)
- Call New BubbleSorter().Sort(lista, callback)
- Assert.AreEqual("García", lista(3).ApellidoPaterno)
- Assert.AreEqual("Lindoro", lista(4).ApellidoMaterno)
- Assert.AreEqual("Enrique", lista(0).Nombre)
- End Sub
- Public Function CompareByApellidos(ByVal a As Persona, ByVal b As Persona) As Integer
- Return String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno))
- End Function
- End Class
Ahora, mostraré como se puede simplificar la escritura de este método con la utilización de Expresiones lambda para que vean que no solo sirven para hacer búsquedas.
Si vemos con atención el método de prueba en el bloque de código anterior, cuando el delegado es creado se le pasa la dirección de memoria de la función compatible que es CompareByApellidos la cual recibe un par de parámetros de tipo Persona. Todo esto está bien, pero para que escribir tanto si se puede hacer de la siguiente manera:
- <TestClass()> _
- Public Class BubbleSorterWithLambdaExpresionTest
- <TestMethod()>
- Public Sub CreateAndSortAList()
- Dim lista As List(Of Persona) = New List(Of Persona)
- lista.Add(New Persona() With {.Nombre = "Noé", .ApellidoPaterno = "García", .ApellidoMaterno = "Urías"})
- lista.Add(New Persona() With {.Nombre = "Enrique", .ApellidoPaterno = "Chávez", .ApellidoMaterno = "Castillo"})
- lista.Add(New Persona() With {.Nombre = "José", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
- lista.Add(New Persona() With {.Nombre = "Julio César", .ApellidoPaterno = "Salazar", .ApellidoMaterno = "Lindoro"})
- lista.Add(New Persona() With {.Nombre = "Arturo", .ApellidoPaterno = "Escobedo", .ApellidoMaterno = "López"})
- Call New BubbleSorter().Sort(lista, _
- Function(a, b) String.Format("{0} {1}", a.ApellidoPaterno, a.ApellidoMaterno).CompareTo(String.Format("{0} {1}", b.ApellidoPaterno, b.ApellidoMaterno)))
- Assert.AreEqual("García", lista(3).ApellidoPaterno)
- Assert.AreEqual("Lindoro", lista(4).ApellidoMaterno)
- Assert.AreEqual("Enrique", lista(0).Nombre)
- End Sub
- End Class
Como podemos observar, las líneas de código son menos (un 30% menos aproximadamente), el código es más simple (una vez que te acostumbras a las expresiones lambda) y más legible. Además se resta bastante la complejidad al dejar de utilizar los delegados que funcionan como intermediarios. Aquí todo es más directo. Lo interesante de este fragmento de código ocurre en las líneas 14 y 15 que en realidad son una sola línea pero la dividí por cuestiones de espacio en el blog.
Analizando este par de líneas podemos ver que ya no fue necesario declarar ningún delegado ni la función CompareByApellidos. En su lugar se utilizó una expresión lambda, la cual cumple con todas las características mencionadas al inicio de este artículo al igual que todas las que se usaron en los ejemplos anteriores.
Los parámetros recibidos “a” y “b” son 2 elementos de la lista diferentes pero subsecuentes y el compilador los va remplazando mediante un bucle que crea cuando compilamos el código por lo que nunca lo vemos.
Y bueno… creo que esto es todo. Espero que si llegaron a leer estas últimas líneas consideren que el título de esta entrega fue el correcto.
0 comentarios:
Publicar un comentario