Ir al contenido principal

Expresiones lambda y LINQ básico

Las expresiones lambda y LINQ (Language Integrated Query) representan una evolución fundamental en la forma de escribir código en C#. Estas características nos permiten crear código más expresivo, conciso y legible, especialmente cuando trabajamos con colecciones de datos. Las expresiones lambda proporcionan una sintaxis compacta para crear funciones anónimas, mientras que LINQ nos ofrece un conjunto uniforme de operaciones para consultar y manipular datos.

Dominar estas herramientas es esencial para cualquier desarrollador de C# moderno, ya que se han convertido en el estándar de la industria para el procesamiento de datos y la creación de consultas elegantes. En este artículo exploraremos desde los conceptos fundamentales hasta las aplicaciones prácticas más comunes.

Expresiones lambda: fundamentos teóricos

Una expresión lambda es una función anónima que puede usarse para crear delegados o tipos de árbol de expresiones. Se caracterizan por su sintaxis concisa y su capacidad para capturar variables del ámbito circundante.

Sintaxis de las expresiones lambda

La sintaxis general de una expresión lambda sigue el patrón:

Componente Descripción Ejemplo
Parámetros de entrada Variables que recibe la lambda x, (x, y), ()
Operador lambda Símbolo que separa parámetros del cuerpo =>
Cuerpo de la expresión Código que se ejecuta x * 2, { return x + y; }

Tipos de expresiones lambda

Tipo Sintaxis Uso
Lambda de expresión parámetros => expresión Para operaciones simples de una línea
Lambda de declaración parámetros => { declaraciones; } Para lógica más compleja que requiere múltiples líneas

Creando nuestras primeras expresiones lambda

Comencemos con ejemplos prácticos para entender cómo funcionan las expresiones lambda:

using System;
using System;

class Program
{
    static void Main()
    {
        // Lambda simple que multiplica por 2
        Func<int, int> duplicar = x => x * 2;
        
        // Lambda con dos parámetros
        Func<int, int, int> sumar = (a, b) => a + b;
        
        // Lambda sin parámetros
        Func<string> obtenerSaludo = () => "¡Hola desde lambda!";
        
        // Lambda con cuerpo de declaración (múltiples líneas)
        Func<int, bool> esPar = numero => 
        {
            Console.WriteLine($"Verificando si {numero} es par");
            return numero % 2 == 0;
        };
        
        // Probando nuestras lambdas
        Console.WriteLine($"Duplicar 5: {duplicar(5)}");
        Console.WriteLine($"Sumar 3 + 7: {sumar(3, 7)}");
        Console.WriteLine(obtenerSaludo());
        Console.WriteLine($"¿Es 8 par? {esPar(8)}");
    }
}

Delegados predefinidos para lambdas

.NET proporciona varios tipos de delegados predefinidos que facilitan el trabajo con expresiones lambda:

Delegado Descripción Sintaxis
Func<T, TResult> Función que toma parámetros y devuelve un valor Func<int, string>
Action<T> Función que toma parámetros pero no devuelve valor Action<string>
Predicate<T> Función que toma un parámetro y devuelve bool Predicate<int>

Veamos estos delegados en acción:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Predicate: función que devuelve bool
        Predicate<int> esMayorQueCinco = x => x > 5;
        
        // Action: función que no devuelve valor
        Action<int> mostrarNumero = numero => Console.WriteLine($"Número: {numero}");
        
        // Func: función que transforma un valor
        Func<int, string> convertirATexto = n => $"El número es {n}";
        
        // Usando los delegados
        Console.WriteLine("Números mayores que 5:");
        foreach (int numero in numeros)
        {
            if (esMayorQueCinco(numero))
            {
                mostrarNumero(numero);
            }
        }
        
        Console.WriteLine("\nConversión a texto:");
        Console.WriteLine(convertirATexto(42));
    }
}

Introducción a LINQ

LINQ (Language Integrated Query) es un conjunto de características que extiende C# con capacidades de consulta nativas. Permite escribir consultas expresivas sobre colecciones de datos utilizando una sintaxis similar a SQL, pero integrada directamente en el lenguaje.

Características principales de LINQ

Característica Descripción
Integración de lenguaje Las consultas son verificadas en tiempo de compilación
Fuente de datos uniforme Misma sintaxis para arrays, listas, XML, bases de datos, etc.
Ejecución diferida Las consultas se ejecutan cuando se necesitan los resultados
Composición Se pueden combinar múltiples operaciones de consulta

Operadores LINQ fundamentales

Los operadores LINQ más utilizados incluyen métodos para filtrar, transformar, ordenar y agrupar datos:

Operadores de filtrado

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        List<string> nombres = new List<string> { "Ana", "Carlos", "Beatriz", "David", "Elena" };
        
        // Where: filtrar elementos que cumplan una condición
        var numerosPares = numeros.Where(n => n % 2 == 0);
        Console.WriteLine("Números pares: " + string.Join(", ", numerosPares));
        
        // Filtrar por longitud de cadena
        var nombresCortos = nombres.Where(nombre => nombre.Length <= 5);
        Console.WriteLine("Nombres cortos: " + string.Join(", ", nombresCortos));
        
        // First y FirstOrDefault: obtener el primer elemento
        var primerPar = numeros.First(n => n % 2 == 0);
        var primerMayorQueVeinte = numeros.FirstOrDefault(n => n > 20);
        
        Console.WriteLine($"Primer número par: {primerPar}");
        Console.WriteLine($"Primer número > 20: {primerMayorQueVeinte}"); // 0 si no existe
    }
}

Operadores de transformación

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };
        List<string> palabras = new List<string> { "hola", "mundo", "csharp", "linq" };
        
        // Select: transformar cada elemento
        var cuadrados = numeros.Select(n => n * n);
        Console.WriteLine("Cuadrados: " + string.Join(", ", cuadrados));
        
        // Transformar strings
        var longitudesPalabras = palabras.Select(p => new { 
            Palabra = p, 
            Longitud = p.Length,
            Mayusculas = p.ToUpper()
        });
        
        Console.WriteLine("\nInformación de palabras:");
        foreach (var info in longitudesPalabras)
        {
            Console.WriteLine($"{info.Palabra} -> {info.Longitud} letras -> {info.Mayusculas}");
        }
        
        // SelectMany: aplanar colecciones
        List<List<int>> numerosAnidados = new List<List<int>>
        {
            new List<int> { 1, 2, 3 },
            new List<int> { 4, 5, 6 },
            new List<int> { 7, 8, 9 }
        };
        
        var numerosAplanados = numerosAnidados.SelectMany(lista => lista);
        Console.WriteLine("\nNúmeros aplanados: " + string.Join(", ", numerosAplanados));
    }
}

Operadores de ordenamiento

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> ciudades = new List<string> { "Madrid", "Barcelona", "Valencia", "Sevilla", "Bilbao" };
        List<int> puntuaciones = new List<int> { 85, 92, 78, 95, 88, 73 };
        
        // OrderBy: ordenar de forma ascendente
        var ciudadesOrdenadas = ciudades.OrderBy(c => c);
        Console.WriteLine("Ciudades ordenadas: " + string.Join(", ", ciudadesOrdenadas));
        
        // OrderByDescending: ordenar de forma descendente
        var puntuacionesDesc = puntuaciones.OrderByDescending(p => p);
        Console.WriteLine("Puntuaciones descendentes: " + string.Join(", ", puntuacionesDesc));
        
        // ThenBy: ordenamiento secundario
        var ciudadesPorLongitud = ciudades
            .OrderBy(c => c.Length)  // Primero por longitud
            .ThenBy(c => c);         // Luego alfabéticamente
            
        Console.WriteLine("Ciudades por longitud y nombre: " + string.Join(", ", ciudadesPorLongitud));
        
        // Reverse: invertir el orden
        var ciudadesInvertidas = ciudades.Reverse();
        Console.WriteLine("Ciudades en orden inverso: " + string.Join(", ", ciudadesInvertidas));
    }
}

Trabajando con objetos complejos

LINQ es especialmente potente cuando trabajamos con colecciones de objetos personalizados:

using System;
using System.Collections.Generic;
using System.Linq;

public class Producto
{
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    public string Categoria { get; set; }
    public bool EnStock { get; set; }
    
    public override string ToString()
    {
        return $"{Nombre} - {Precio:C} ({Categoria}) - {(EnStock ? "En stock" : "Agotado")}";
    }
}

class Program
{
    static void Main()
    {
        List<Producto> productos = new List<Producto>
        {
            new Producto { Nombre = "Portátil", Precio = 899.99m, Categoria = "Tecnología", EnStock = true },
            new Producto { Nombre = "Mesa", Precio = 149.50m, Categoria = "Muebles", EnStock = false },
            new Producto { Nombre = "Smartphone", Precio = 699.00m, Categoria = "Tecnología", EnStock = true },
            new Producto { Nombre = "Silla", Precio = 89.99m, Categoria = "Muebles", EnStock = true },
            new Producto { Nombre = "Tablet", Precio = 299.99m, Categoria = "Tecnología", EnStock = false }
        };
        
        // Filtrar productos en stock de tecnología
        var tecnoEnStock = productos
            .Where(p => p.Categoria == "Tecnología" && p.EnStock)
            .OrderBy(p => p.Precio);
            
        Console.WriteLine("Productos de tecnología en stock (por precio):");
        foreach (var producto in tecnoEnStock)
        {
            Console.WriteLine($"  {producto}");
        }
        
        // Obtener el precio promedio por categoría
        var preciosPromedio = productos
            .GroupBy(p => p.Categoria)
            .Select(grupo => new {
                Categoria = grupo.Key,
                PrecioPromedio = grupo.Average(p => p.Precio),
                Cantidad = grupo.Count()
            });
            
        Console.WriteLine("\nPrecios promedio por categoría:");
        foreach (var info in preciosPromedio)
        {
            Console.WriteLine($"  {info.Categoria}: {info.PrecioPromedio:C} ({info.Cantidad} productos)");
        }
        
        // Producto más caro en stock
        var masCaroEnStock = productos
            .Where(p => p.EnStock)
            .OrderByDescending(p => p.Precio)
            .FirstOrDefault();
            
        Console.WriteLine($"\nProducto más caro en stock: {masCaroEnStock}");
    }
}

Operadores de agregación

Los operadores de agregación nos permiten obtener valores resumidos de nuestras colecciones:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> ventas = new List<int> { 1200, 800, 1500, 900, 2000, 1100, 750 };
        List<string> comentarios = new List<string> { "Excelente", "Bueno", "Regular", "Malo", "Excelente" };
        
        // Operadores numéricos
        Console.WriteLine($"Total de ventas: {ventas.Sum()}");
        Console.WriteLine($"Promedio de ventas: {ventas.Average():F2}");
        Console.WriteLine($"Venta máxima: {ventas.Max()}");
        Console.WriteLine($"Venta mínima: {ventas.Min()}");
        Console.WriteLine($"Número de ventas: {ventas.Count()}");
        
        // Count con condición
        var ventasAltas = ventas.Count(v => v > 1000);
        Console.WriteLine($"Ventas superiores a 1000: {ventasAltas}");
        
        // Any y All
        bool hayVentasAltas = ventas.Any(v => v > 1500);
        bool todasVentasPositivas = ventas.All(v => v > 0);
        
        Console.WriteLine($"¿Hay ventas > 1500? {hayVentasAltas}");
        Console.WriteLine($"¿Todas las ventas son positivas? {todasVentasPositivas}");
        
        // Aggregate: operación personalizada
        var productoVentas = ventas.Aggregate((acumulado, actual) => acumulado * actual);
        Console.WriteLine($"Producto de todas las ventas: {productoVentas}");
        
        // Distinct: elementos únicos
        var comentariosUnicos = comentarios.Distinct();
        Console.WriteLine("Comentarios únicos: " + string.Join(", ", comentariosUnicos));
    }
}

Encadenamiento de operaciones LINQ

Una de las características más potentes de LINQ es la capacidad de encadenar múltiples operaciones para crear consultas complejas:

using System;
using System.Collections.Generic;
using System.Linq;

public class Estudiante
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    public List<int> Calificaciones { get; set; }
    
    public double Promedio => Calificaciones.Any() ? Calificaciones.Average() : 0;
}

class Program
{
    static void Main()
    {
        List<Estudiante> estudiantes = new List<Estudiante>
        {
            new Estudiante { Nombre = "Ana", Edad = 20, Calificaciones = new List<int> { 85, 90, 88 } },
            new Estudiante { Nombre = "Carlos", Edad = 19, Calificaciones = new List<int> { 78, 85, 80 } },
            new Estudiante { Nombre = "Beatriz", Edad = 21, Calificaciones = new List<int> { 92, 95, 89 } },
            new Estudiante { Nombre = "David", Edad = 20, Calificaciones = new List<int> { 70, 75, 73 } },
            new Estudiante { Nombre = "Elena", Edad = 22, Calificaciones = new List<int> { 88, 90, 85 } }
        };
        
        // Consulta compleja encadenada
        var estudiantesDestacados = estudiantes
            .Where(e => e.Promedio >= 85)           // Filtrar por promedio
            .OrderByDescending(e => e.Promedio)     // Ordenar por promedio
            .Take(3)                                // Tomar los 3 mejores
            .Select(e => new {                      // Proyectar a objeto anónimo
                Nombre = e.Nombre,
                Edad = e.Edad,
                Promedio = e.Promedio,
                Calificacion = e.Promedio >= 90 ? "Excelente" : "Bueno"
            });
        
        Console.WriteLine("Top 3 estudiantes destacados:");
        foreach (var estudiante in estudiantesDestacados)
        {
            Console.WriteLine($"{estudiante.Nombre} ({estudiante.Edad} años): " +
                            $"{estudiante.Promedio:F2} - {estudiante.Calificacion}");
        }
        
        // Análisis adicional con múltiples operaciones
        var estadisticasPorEdad = estudiantes
            .GroupBy(e => e.Edad)                   // Agrupar por edad
            .Where(grupo => grupo.Count() > 1)      // Solo edades con múltiples estudiantes
            .Select(grupo => new {
                Edad = grupo.Key,
                Cantidad = grupo.Count(),
                PromedioGrupal = grupo.Average(e => e.Promedio)
            })
            .OrderBy(stats => stats.Edad);
        
        Console.WriteLine("\nEstadísticas por edad (grupos con 2+ estudiantes):");
        foreach (var stats in estadisticasPorEdad)
        {
            Console.WriteLine($"Edad {stats.Edad}: {stats.Cantidad} estudiantes, " +
                            $"promedio grupal: {stats.PromedioGrupal:F2}");
        }
    }
}

Sintaxis de consulta vs sintaxis de método

LINQ ofrece dos sintaxis diferentes para escribir consultas: la sintaxis de consulta (similar a SQL) y la sintaxis de método (que hemos estado usando):

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Sintaxis de método (la que hemos usado hasta ahora)
        var paresMetodo = numeros
            .Where(n => n % 2 == 0)
            .Select(n => n * n)
            .OrderByDescending(n => n);
        
        // Sintaxis de consulta (similar a SQL)
        var paresConsulta = from n in numeros
                           where n % 2 == 0
                           select n * n
                           orderby n * n descending
                           select n * n;
        
        Console.WriteLine("Resultado con sintaxis de método:");
        Console.WriteLine(string.Join(", ", paresMetodo));
        
        Console.WriteLine("\nResultado con sintaxis de consulta:");
        Console.WriteLine(string.Join(", ", paresConsulta));
        
        // Ejemplo más complejo con sintaxis de consulta
        List<string> palabras = new List<string> { "hola", "mundo", "csharp", "linq", "expresiones" };
        
        var consulpaCompleja = from palabra in palabras
                              where palabra.Length > 4
                              let mayuscula = palabra.ToUpper()
                              orderby palabra.Length, palabra
                              select new { 
                                  Original = palabra, 
                                  Mayuscula = mayuscula, 
                                  Longitud = palabra.Length 
                              };
        
        Console.WriteLine("\nConsulta compleja:");
        foreach (var resultado in consulpaCompleja)
        {
            Console.WriteLine($"{resultado.Original} -> {resultado.Mayuscula} ({resultado.Longitud})");
        }
    }
}

Resumen

Las expresiones lambda y LINQ representan herramientas fundamentales en el ecosistema de C# moderno. Las expresiones lambda nos proporcionan una sintaxis elegante para crear funciones anónimas, mientras que LINQ nos ofrece un poderoso conjunto de operadores para consultar y manipular datos de forma declarativa.

Hemos explorado desde los conceptos básicos de las lambdas hasta las consultas LINQ más sofisticadas, incluyendo operadores de filtrado, transformación, ordenamiento y agregación. La capacidad de encadenar operaciones LINQ nos permite crear consultas expresivas y eficientes que procesan datos de manera intuitiva y legible. Estas herramientas no solo mejoran la productividad del desarrollador, sino que también hacen que el código sea más mantenible y comprensible, estableciendo las bases para técnicas avanzadas como la programación funcional y el procesamiento de datos en aplicaciones modernas.