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.