94 - Expresiones lambda


Una expresión lambda es una función anónima que normalmente se la utiliza para enviarla como parámetro a un método para ser evaluada en el mismo.

Problema

Definir un método estático llamado Operar. Llegan como parámetro dos enteros y la referencia a una expresión lambda (lo debe recibir un delegado). En el bloque del método llamar a la función que llega como parámetro y enviar los dos primeros parámetros.

Desde la función Main llamar a Operar y enviar distintas expresiones lambdas que permitan sumar, restar y elevar el primer valor al segundo .

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lambda1
{
    delegate int Operacion(int x1, int x2);

    class Program
    {

        public static int Operar(int x1, int x2, Operacion delegado)
        {
            return delegado(x1, x2);
        }

        static void Main(string[] args)
        {
            var suma = Program.Operar(10, 3, (x, y) => { return x + y; } );
            Console.WriteLine("La suma de 10 y 3 es {0}", suma);
            var resta = Program.Operar(10, 3, (x, y) => { return x - y; });
            Console.WriteLine("La resta de 10 y 3 es {0}", resta);
            var elevadoOctava = Program.Operar(2, 8, (x, y) => {
                var valor = x;
                for (int f = 1; f < y; f++)
                    valor = valor * x;
                return valor;
            });
            Console.WriteLine("2 elevado a la 8 es {0}", elevadoOctava);
            Console.ReadKey();
        }
    }
}

Declaramos un delegado llamado Operación cuya firma es que recibe dos enteros y retorna un entero:

    delegate int Operacion(int x1, int x2);

En la clase Program definimos un método estático llamado Operar que recibe dos enteros, un delegado de tipo Operación y retorna un entero:

        public static int Operar(int x1, int x2, Operacion delegado)
        {
            return delegado(x1, x2);
        }

Dentro del método Operar llamamos a la referencia de la función anónima y el valor que esta nos retorna la retornamos mediante un return.

En el método Main llamamos al método Operar y le enviamos un 10, un 3 y una expresión lambda:

            var suma = Program.Operar(10, 3, (x, y) => { return x + y; } );

Una expresión lambda tiene la siguiente sintaxis para este problema:

(x, y) => { return x + y; }

Podemos decir que es una función anónima ya que no tiene nombre, tiene dos parámetros llamados x e y. Luego de operador => indicamos entre llaves el algoritmo propiamente de la función anónima.

Cuando llamamos a Operar sabemos que dentro de dicho método llamará y ejecutará la expresión lambda.

La función anónima debe respetar la firma del delegado Operación, es decir que reciba dos enteros y retorne un entero.

Para poder obtener la resta de dos enteros llamamos al método Operar y le pasamos dos enteros y una expresión lambda que recibe dos enteros y retorna la diferencia entre ellos:

            var resta = Program.Operar(10, 3, (x, y) => { return x - y; });
            Console.WriteLine("La resta de 10 y 3 es {0}", resta);

Como podemos ver nuestro método Operar es bastante general y permite indicar que operaciones hacer con los datos recibidos indicando mediante una expresión lambda el algoritmo a ejecutar.

Para elevar el primer valor a una potencia indicada en el segundo valor debemos implementar el siguiente algoritmo en la expresión lambda:

            var elevadoOctava = Program.Operar(2, 8, (x, y) => {
                var valor = x;
                for (int f = 1; f < y; f++)
                    valor = valor * x;
                return valor;
            });
            Console.WriteLine("2 elevado a la 8 es {0}", elevadoOctava);

Lo más común es el empleo de lambdas con un algoritmo muy corto. En este ejemplo el más largo es el que eleva un número a una determinada potencia.

Acotaciones

Cuando el algoritmo de una expresión lambda tiene una sola instrucción lo más común es no disponer las llaves ni la palabra clave return (esto hace nuestro expresión lambda más concisa):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lambda1
{
    delegate int Operacion(int x1, int x2);

    class Program
    {

        public static int Operar(int x1, int x2, Operacion delegado)
        {
            return delegado(x1, x2);
        }

        static void Main(string[] args)
        {
            var suma = Program.Operar(10, 3, (x, y) => x + y );
            Console.WriteLine("La suma de 10 y 3 es {0}", suma);
            var resta = Program.Operar(10, 3, (x, y) => x - y );
            Console.WriteLine("La resta de 10 y 3 es {0}", resta);
            var elevadoOctava = Program.Operar(2, 8, (x, y) => {
                var valor = x;
                for (int f = 1; f < y; f++)
                    valor = valor * x;
                return valor;
            });
            Console.WriteLine("2 elevado a la 8 es {0}", elevadoOctava);
            Console.ReadKey();
        }
    }
}

Como vemos para sumar la expresión lambda no requiere las llaves ni la palabra clave return:

            var suma = Program.Operar(10, 3, (x, y) => x + y );

Lo mismo para calcular la resta:

            var resta = Program.Operar(10, 3, (x, y) => x - y );

Para elevar un valor a una determinada potencia el algoritmo tiene más de una instrucción, luego debe ir en forma obligatoria entre llaves:

            var elevadoOctava = Program.Operar(2, 8, (x, y) => {
                var valor = x;
                for (int f = 1; f < y; f++)
                    valor = valor * x;
                return valor;
            });

Para entender el orden de ejecución de los algoritmos podemos ejecutar cada instrucción una a una presionando la tecla F11 del Visual Studio .net:

ejecución paso a paso expresiones lambda en C# con el visual studio .net

Podremos comprobar como llamamos al método Operar y comienza su ejecución, en este salga nuevamente al método Main a ejecutar el algoritmo de la expresión lambda x + y. Ahora nuevamente regresa al método Operar y recién ejecuta el return del método Operar.

El orden temporal de ejecución de métodos lo podemos resumir con 4 pasos:

ejecución paso a paso expresiones lambda en C# con el visual studio .net
  1. Llamamos al método Operar y le enviamos dos enteros y una expresión lambda.
  2. Desde el método Operar llamamos a la función anónima y le enviamos dos enteros, que son los recibidos en el método Operar.
  3. Se ejecuta el algoritmo de la función anónima (suma los parámetros x e y y los retorna.
  4. Finaliza la ejecución del método Operar retornando el valor devuelto en la llamada al delegado.

Problema

Declarar una clase Estudiante con dos propiedades que permitan almacenar el nombre y su nota.
Por otro lado declarar una clase llamada Curso que tenga un campo de tipo vector con componentes de tipo Estudiante. Definir el vector de 5 elementos. Permitir cargar estudiantes en el curso e imprimir en forma completa todos los estudiantes.

Definir un método en la clase Curso que reciba como parámetro un delegado que tiene un parámetro de tipo entero y retorna un true o false indicando si se debe imprimir los datos del estudiante.

En la función Main definir un objeto de la clase Curso, cargar 5 estudiantes. Luego imprimir:
Todos los estudiantes.
Todos los estudiantes que tienen una nota mayor o igual a 7.
Todos los estudiantes que tienen un 2.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lambda2
{
    public delegate bool Comparacion(int elemento);

    public class Estudiante
    {
        public String Nombre { get; set; }
        public int Nota { get; set; }
    }

    public class Curso
    {
        private Estudiante[] vec = new Estudiante[5];

        public void Cargar(int pos, Estudiante est)
        {
            vec[pos] = est;
        }

        public void ImprimirTodo()
        {
            foreach(var elemento in vec)
                Console.WriteLine("Nombre: {0} Nota: {1}", elemento.Nombre, elemento.Nota);
        }

        public void ImprimirSi(Comparacion compara)
        {
            foreach(var elemento in vec) 
                if (compara(elemento.Nota))
                    Console.WriteLine("Nombre: {0} Nota: {1}", elemento.Nombre, elemento.Nota);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Curso curso1 = new Curso();
            curso1.Cargar(0, new Estudiante { Nombre = "jose", Nota = 9 });
            curso1.Cargar(1, new Estudiante { Nombre = "ana", Nota = 10 });
            curso1.Cargar(2, new Estudiante { Nombre = "luis", Nota = 2 });
            curso1.Cargar(3, new Estudiante { Nombre = "pedro", Nota = 7 });
            curso1.Cargar(4, new Estudiante { Nombre = "carla", Nota = 3 });
            Console.WriteLine("Listado completo de alumnos");
            curso1.ImprimirTodo();
            Console.WriteLine("Listado completo de estudiantes con notas mayores o iguales a 7");
            curso1.ImprimirSi((nota) => nota >= 7);
            Console.WriteLine("Listado completo de estudiantes que tienen un 2");
            curso1.ImprimirSi((nota) => nota == 2);
            Console.WriteLine("Listado completo de estudiantes empleando el método ImprimirSi");
            curso1.ImprimirSi((nota) => true);
            Console.ReadKey();
        }
    }
}

Primero declaramos un delegado que recibe un parámetro entero y retorna un valor bool (es decir un true o false):

    public delegate bool Comparacion(int elemento);

Declaramos la clase Estudiante con sus dos propiedades:

    public class Estudiante
    {
        public String Nombre { get; set; }
        public int Nota { get; set; }
    }

Declaramos la clase Curso con un atributo de tipo vector de 5 elementos de tipo Estudiante:

    public class Curso
    {
        private Estudiante[] vec = new Estudiante[5];

El método Cargar recibe como parámetro un objeto de la clase Estudiante y la posición dentro del vector donde almacenarlo:

        public void Cargar(int pos, Estudiante est)
        {
            vec[pos] = est;
        }

El método ImprimirTodo muestra el vector de estudiantes en forma completo:

        public void ImprimirTodo()
        {
            foreach(var elemento in vec)
                Console.WriteLine("Nombre: {0} Nota: {1}", elemento.Nombre, elemento.Nota);
        }

El método ImprimirSi recibe como parámetro un tipo de dato Comparacion que es un delegate. Dentro del método recorremos con un foreach cada elemento del vector y llamamos a la expresión lambda que nos retorna si debemos imprimir dicho estudiante:

        public void ImprimirSi(Comparacion compara)
        {
            foreach(var elemento in vec) 
                if (compara(elemento.Nota))
                    Console.WriteLine("Nombre: {0} Nota: {1}", elemento.Nombre, elemento.Nota);
        }
    }

Como podemos ver podemos llamar a la expresión lambda varias veces, una por cada nota de estudiante almacenada en el vector.

En el método Main creamos un objeto de la clase Curso y almacenamos 5 estudiantes:

        static void Main(string[] args)
        {
            Curso curso1 = new Curso();
            curso1.Cargar(0, new Estudiante { Nombre = "jose", Nota = 9 });
            curso1.Cargar(1, new Estudiante { Nombre = "ana", Nota = 10 });
            curso1.Cargar(2, new Estudiante { Nombre = "luis", Nota = 2 });
            curso1.Cargar(3, new Estudiante { Nombre = "pedro", Nota = 7 });
            curso1.Cargar(4, new Estudiante { Nombre = "carla", Nota = 3 });

Para imprimir todos los estudiantes llamamos al método ImprimirTodo:

            Console.WriteLine("Listado completo de estudiantes");
            curso1.ImprimirTodo();

Para imprimir los datos de estudiantes que tienen una nota igual o superior a 7 llamamos al método ImprimirSi y le pasamos una expresión lambda donde analizamos si la nota que llega como parámetro a la expresión lambda cumple con la condición de ser mayor o igual a 7:

            Console.WriteLine("Listado completo de estudiantes con notas mayores o iguales a 7");
            curso1.ImprimirSi((nota) => nota >= 7);

Para imprimir todos los estudiantes que tienen una nota de 2 volvemos a llamar al método ImprimirSi pasando otro expresión lambda:

            Console.WriteLine("Listado completo de estudiantes que tienen un 2");
            curso1.ImprimirSi((nota) => nota == 2);

También podemos utilizar el método ImprmirSi para imprimir todos los estudiantes empleando una expresión lambda cuyo algoritmo retorna siempre true:

            Console.WriteLine("Listado completo de estudiantes empleando el método ImprimirSi");
            curso1.ImprimirSi((nota) => true);

Acotaciones

El lenguaje C# nos permite eliminar los paréntesis cuando la expresión lambda tiene un solo parámetro, luego el código queda con una sintaxis más concisa:

            Console.WriteLine("Listado completo de estudiantes con notas mayores o iguales a 7");
            curso1.ImprimirSi(nota => nota >= 7);
            Console.WriteLine("Listado completo de estudiantes que tienen un 2");
            curso1.ImprimirSi(nota => nota == 2);
            Console.WriteLine("Listado completo de estudiantes empleando el método ImprimirSi");
            curso1.ImprimirSi(nota => true);

Problema

Plantear un método estático que reciba un string y retorne otro. Mediante una expresión lambda analizar cada caracter del string original y verificar si lo debe agregar al string de retorno.
El método debe llamarse Filtrar y luego debemos acceder al mismo desde la Main para obtener:
Todas las vocales que contiene un string.
Solo los dígitos.
Solo los caracteres minúsculas.
Solo los caracteres y números.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lambda3
{
    public delegate bool Agrega(char caracter);

    class Program
    {
        public static string Filtrar(string cadena, Agrega agrega)
        {
            StringBuilder cadenaNueva = new StringBuilder();
            foreach(var caracter in cadena)
                if (agrega(caracter))
                    cadenaNueva.Append(caracter);
            return cadenaNueva.ToString();
        }

        static void Main(string[] args)
        {
            var cadena = "1 Hola 2 Mundo 3";
            Console.WriteLine("Cadena original:{0}", cadena);
            var cad1 = Program.Filtrar(cadena , caracter => caracter == 'a' || caracter == 'e' || caracter == 'i' || caracter == 'o' || caracter == 'u' ||
                                                             caracter == 'A' || caracter == 'E' || caracter == 'I' || caracter == 'O' || caracter == 'U');
            Console.WriteLine("Solo las vocales: {0}", cad1);
            var cad2 = Program.Filtrar(cadena, caracter => char.IsDigit(caracter));
            Console.WriteLine("Solo los dígitos: {0}",cad2);
            var cad3 = Program.Filtrar(cadena, caracter => char.IsLower(caracter));
            Console.WriteLine("Solo los caracteres minúsculas: {0}", cad3);
            var cad4 = Program.Filtrar(cadena, caracter => char.IsLetter(caracter) || char.IsNumber(caracter));
            Console.WriteLine("Solo los caracteres y números: {0}", cad4);
            Console.ReadKey();
        }
    }
}

La estructura del método delegado es:

    public delegate bool Agrega(char caracter);

El método Filtrar recibe un string y la referencia a la expresión lambda:

        public static string Filtrar(string cadena, Agrega agrega)

Creamos un StringBuilder para generar el string a retornar:

        {
            StringBuilder cadenaNueva = new StringBuilder();

Mediante un foreach recorremos el string original y por cada caracter llamamos mediante el delegado a la expresión lambda que nos indica si debemos añadir o no el caracter al string de retorno:

            foreach(var caracter in cadena)
                if (agrega(caracter))
                    cadenaNueva.Append(caracter);
            return cadenaNueva.ToString();
        }

En la función Main definimos un string al cual le aplicaremos distintos filtros:

        static void Main(string[] args)
        {
            var cadena = "1 Hola 2 Mundo 3";
            Console.WriteLine("Cadena original:{0}", cadena);

Para generar un string solo con las vocales contenidas en otro string debemos pasar la siguiente expresión lambda:

            var cad1 = Program.Filtrar(cadena , caracter => caracter == 'a' || caracter == 'e' || caracter == 'i' || caracter == 'o' || caracter == 'u' ||
                                                             caracter == 'A' || caracter == 'E' || caracter == 'I' || caracter == 'O' || caracter == 'U');
            Console.WriteLine("Solo las vocales: {0}", cad1);

Para filtrar solo los dígitos contenidos en un string en el algoritmo de la expresión lambda verificamos cada caracter si se trata de un dígito:

            var cad2 = Program.Filtrar(cadena, caracter => char.IsDigit(caracter));
            Console.WriteLine("Solo los dígitos: {0}",cad2);

De forma similar podemos filtrar si es un caracter minúscula:

            var cad3 = Program.Filtrar(cadena, caracter => char.IsLower(caracter));
            Console.WriteLine("Solo los caracteres minúsculas: {0}", cad3);

Por último si queremos filtrar solo las letras o números debemos implementar:

            var cad4 = Program.Filtrar(cadena, caracter => char.IsLetter(caracter) || char.IsNumber(caracter));
            Console.WriteLine("Solo los caracteres y números: {0}", cad4);

Retornar