CSharp
Tutorial CSharp: Expresiones Lambda
Aprende la sintaxis y uso avanzado de expresiones lambda en C#, incluyendo captura de variables y cierres léxicos para mejorar tu código.
Aprende CSharp y certifícateSintaxis de expresiones lambda
Las expresiones lambda en C# representan una forma concisa de escribir funciones anónimas. Estas expresiones permiten definir métodos sin necesidad de declarar una clase o un método formal, lo que resulta especialmente útil para operaciones breves que no necesitan ser reutilizadas en múltiples lugares.
Estructura básica
La sintaxis básica de una expresión lambda consta de tres elementos principales:
(parámetros) => expresión_o_bloque
Donde:
- Los parámetros son las entradas que recibe la función
- El operador
=>
(flecha lambda) separa los parámetros del cuerpo - La expresión o bloque contiene el código a ejecutar
Variantes de sintaxis
Existen diferentes formas de escribir expresiones lambda dependiendo de la cantidad de parámetros y la complejidad del cuerpo de la función:
- Lambda sin parámetros:
// Sin parámetros
Action mostrarMensaje = () => Console.WriteLine("Hola mundo");
mostrarMensaje(); // Imprime: Hola mundo
- Lambda con un solo parámetro:
// Con un parámetro (los paréntesis son opcionales)
Func<int, int> cuadrado = x => x * x;
Console.WriteLine(cuadrado(5)); // Imprime: 25
Cuando la lambda tiene un solo parámetro, los paréntesis son opcionales:
// Estas dos expresiones son equivalentes
Func<string, int> longitud1 = s => s.Length;
Func<string, int> longitud2 = (s) => s.Length;
- Lambda con múltiples parámetros:
// Con múltiples parámetros (paréntesis obligatorios)
Func<int, int, int> sumar = (a, b) => a + b;
Console.WriteLine(sumar(3, 4)); // Imprime: 7
Expresiones vs. bloques de instrucciones
Las expresiones lambda pueden tener dos formas de cuerpo:
- Expresión lambda de expresión: consiste en una única expresión cuyo resultado se devuelve implícitamente.
// Lambda de expresión (return implícito)
Func<int, bool> esPar = n => n % 2 == 0;
- Expresión lambda de bloque: contiene múltiples instrucciones entre llaves y requiere una instrucción
return
explícita si se desea devolver un valor.
// Lambda de bloque (return explícito)
Func<int, int> factorial = n => {
int resultado = 1;
for (int i = 1; i <= n; i++)
resultado *= i;
return resultado;
};
Console.WriteLine(factorial(5)); // Imprime: 120
Tipos de parámetros
Normalmente, el compilador puede inferir los tipos de los parámetros, pero también es posible especificarlos explícitamente:
// Tipos inferidos
Func<int, int> duplicar1 = x => x * 2;
// Tipos explícitos
Func<int, int> duplicar2 = (int x) => x * 2;
Especificar los tipos puede ser necesario en situaciones donde la inferencia de tipos no es suficiente:
// Especificando tipos para mayor claridad
var compararPorLongitud = (string s1, string s2) => s1.Length.CompareTo(s2.Length);
Uso con delegados predefinidos
C# proporciona varios tipos de delegados predefinidos que facilitan el uso de lambdas:
- Action: para lambdas que no devuelven valor (void)
// Action sin parámetros
Action saludar = () => Console.WriteLine("Hola");
// Action con un parámetro
Action<string> saludarA = nombre => Console.WriteLine($"Hola, {nombre}");
// Action con múltiples parámetros
Action<string, int> repetirSaludo = (nombre, veces) => {
for (int i = 0; i < veces; i++)
Console.WriteLine($"Hola, {nombre}");
};
- Func: para lambdas que devuelven un valor
// Func con un parámetro de entrada y uno de salida
Func<int, string> convertirATexto = n => n.ToString();
// Func con múltiples parámetros de entrada
Func<double, double, double> potencia = (base1, exponente) => Math.Pow(base1, exponente);
- Predicate: para lambdas que devuelven un booleano (es un caso especial de Func)
// Predicate (equivalente a Func<T, bool>)
Predicate<int> esMayorDeEdad = edad => edad >= 18;
Parámetros descartados
Si un parámetro no se utiliza en el cuerpo de la lambda, se puede indicar con un guion bajo:
// Ignorando el segundo parámetro
Func<string, string, string> usarSoloPrimero = (primero, _) => primero.ToUpper();
// Múltiples parámetros descartados
Action<int, string, DateTime> procesarSoloEntero = (num, _, _) => {
Console.WriteLine($"Procesando: {num}");
};
Compatibilidad con tipos de delegados
Las expresiones lambda son compatibles con cualquier tipo de delegado que coincida con su firma:
// Delegado personalizado
delegate bool ValidadorNumero(int numero);
// Asignando una lambda a un delegado personalizado
ValidadorNumero validarPar = n => n % 2 == 0;
Console.WriteLine(validarPar(4)); // Imprime: True
Esta flexibilidad permite usar lambdas en cualquier contexto donde se espere un delegado, lo que facilita la implementación de patrones como estrategia, observador o callback de manera concisa.
Captura de variables
Las expresiones lambda en C# no solo pueden utilizar los parámetros que reciben, sino que también pueden acceder y capturar variables del ámbito donde fueron definidas. Esta característica, conocida como cierre léxico o closure, permite que las lambdas mantengan referencias a variables externas incluso después de que el método original haya finalizado su ejecución.
Funcionamiento básico de la captura
Cuando una expresión lambda utiliza una variable definida fuera de su cuerpo, el compilador de C# automáticamente captura esa variable:
string saludo = "Hola";
Action mensaje = () => Console.WriteLine($"{saludo}, mundo!");
mensaje(); // Imprime: Hola, mundo!
En este ejemplo, la lambda captura la variable saludo
del ámbito exterior. Esto permite que la lambda acceda al valor de esta variable cuando se ejecuta.
Captura de variables locales
Las variables locales capturadas tienen un comportamiento especial:
void DemostrarCaptura()
{
int contador = 0;
// La lambda captura 'contador' por referencia
Action incrementar = () => {
contador++;
Console.WriteLine($"Contador: {contador}");
};
incrementar(); // Imprime: Contador: 1
incrementar(); // Imprime: Contador: 2
Console.WriteLine($"Valor final: {contador}"); // Imprime: Valor final: 2
}
Cuando ejecutamos este código, podemos observar que la variable contador
es modificada por la lambda y los cambios persisten entre llamadas. Esto ocurre porque la variable es capturada por referencia, no por valor.
Ciclo de vida extendido
Una característica importante de las variables capturadas es que su ciclo de vida se extiende mientras exista alguna referencia a la lambda que las captura:
Func<int> CrearContador()
{
int contador = 0;
return () => ++contador;
}
var contador1 = CrearContador();
var contador2 = CrearContador();
Console.WriteLine(contador1()); // Imprime: 1
Console.WriteLine(contador1()); // Imprime: 2
Console.WriteLine(contador2()); // Imprime: 1 (contador independiente)
En este ejemplo, aunque el método CrearContador()
ya terminó su ejecución, la variable contador
sigue existiendo porque fue capturada por la lambda. Cada llamada a CrearContador()
crea una nueva instancia de contador
, por lo que contador1
y contador2
mantienen estados independientes.
Captura de variables en bucles
Un caso particular que requiere atención es la captura de variables dentro de bucles:
// Comportamiento en versiones anteriores a C# 5
var acciones = new List<Action>();
for (int i = 0; i < 3; i++)
{
acciones.Add(() => Console.WriteLine(i));
}
// En versiones anteriores a C# 5, esto imprimiría tres veces "3"
foreach (var accion in acciones)
{
accion();
}
En versiones anteriores a C# 5, todas las lambdas capturaban la misma variable i
, cuyo valor final era 3. A partir de C# 5, cada iteración del bucle crea una nueva variable, por lo que el código anterior imprimiría 0, 1 y 2 como se esperaría.
Para evitar confusiones en cualquier versión, se puede crear una copia local de la variable:
var acciones = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copiaLocal = i; // Crear una nueva variable en cada iteración
acciones.Add(() => Console.WriteLine(copiaLocal));
}
// Esto imprimirá 0, 1, 2 en cualquier versión de C#
foreach (var accion in acciones)
{
accion();
}
Captura de variables de instancia y estáticas
Las lambdas también pueden capturar variables de instancia y estáticas:
class Contador
{
private int _valor = 0;
private static int _contadorGlobal = 0;
public Action ObtenerIncrementador()
{
return () => {
_valor++; // Captura variable de instancia
_contadorGlobal++; // Captura variable estática
Console.WriteLine($"Instancia: {_valor}, Global: {_contadorGlobal}");
};
}
}
En este caso, la lambda captura this
implícitamente para acceder a _valor
, y también captura la variable estática _contadorGlobal
.
Implicaciones de rendimiento
La captura de variables tiene algunas implicaciones de rendimiento que conviene conocer:
- Creación de clases: Internamente, el compilador crea una clase para almacenar las variables capturadas.
- Asignación en el heap: Las variables capturadas se mueven del stack al heap, lo que puede afectar al rendimiento.
void MetodoConLambda()
{
int x = 10; // Variable local (stack)
Action lambda = () => Console.WriteLine(x);
// Ahora 'x' se almacena en el heap como parte de una clase generada
lambda();
}
Captura de parámetros ref y out
Los parámetros ref
y out
no pueden ser capturados directamente por lambdas:
// Esto NO compila
void Procesar(ref int valor)
{
Action lambda = () => valor++; // Error: Cannot use ref or out parameter in lambda
}
Para solucionar esto, se puede crear una variable local intermedia:
void Procesar(ref int valor)
{
int copiaLocal = valor;
Action lambda = () => copiaLocal++;
lambda();
valor = copiaLocal; // Actualizar el parámetro ref manualmente
}
Uso práctico: Memorización de funciones
Un uso práctico de la captura de variables es implementar la memorización (caching) de resultados de funciones:
Func<int, int> CrearCalculadoraConMemoria(Func<int, int> calculoComplejo)
{
var cache = new Dictionary<int, int>();
return n => {
if (!cache.ContainsKey(n))
{
Console.WriteLine($"Calculando para {n}...");
cache[n] = calculoComplejo(n);
}
return cache[n];
};
}
// Uso
Func<int, int> fibonacci = CrearCalculadoraConMemoria(n =>
n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2)
);
Console.WriteLine(fibonacci(10)); // Calcula y almacena resultados intermedios
Console.WriteLine(fibonacci(10)); // Usa valores en caché
En este ejemplo, la lambda captura el diccionario cache
para almacenar resultados previos, mejorando significativamente el rendimiento para llamadas repetidas.
Evitar problemas comunes
Al trabajar con captura de variables, es importante evitar estos problemas comunes:
- Modificación de variables capturadas: Ten cuidado al modificar variables capturadas desde múltiples lambdas, ya que pueden producirse condiciones de carrera.
int contador = 0;
var incrementadores = Enumerable.Range(0, 10)
.Select(_ => new Thread(() => {
for (int i = 0; i < 1000; i++)
contador++; // Posible condición de carrera
}))
.ToList();
incrementadores.ForEach(t => t.Start());
incrementadores.ForEach(t => t.Join());
// El resultado podría no ser 10000
Console.WriteLine(contador);
- Captura no intencionada: Evita capturar más variables de las necesarias, especialmente objetos grandes.
// Mal: captura toda la lista
var lista = new List<string> { "uno", "dos", "tres" };
Func<string, bool> buscar = s => lista.Contains(s);
// Mejor: captura solo lo necesario
var conjunto = new HashSet<string>(lista);
Func<string, bool> buscarOptimizado = s => conjunto.Contains(s);
La captura de variables es una característica potente que permite crear cierres léxicos y funciones que mantienen estado, pero debe usarse con conocimiento de sus implicaciones para evitar problemas de rendimiento o comportamientos inesperados.
Ejercicios de esta lección Expresiones Lambda
Evalúa tus conocimientos de esta lección Expresiones Lambda con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
CRUD en C# de modelo Customer sobre una lista
Arrays y listas
Objetos
Excepciones
Eventos
Lambdas
Diccionarios en C#
Variables y constantes
Tipos de datos
Herencia
Operadores
Uso de consultas LINQ
Clases y encapsulación
Uso de consultas LINQ
Excepciones
Control de flujo
Eventos
Diccionarios
Tipos de datos
Conjuntos, colas y pilas
Lambdas
Conjuntos, colas y pilas
Uso de async y await
Tareas
Constructores y destructores
Operadores
Arrays y listas
Polimorfismo
Polimorfismo
Variables y constantes
Proyecto colecciones y LINQ en C#
Clases y encapsulación
Creación de proyecto C#
Uso de async y await
Funciones
Delegados
Delegados
Constructores y destructores
Objetos
Control de flujo
Funciones
Tareas
Proyecto sintaxis en C#
Herencia C Sharp
OOP en C Sharp
Diccionarios
Introducción a C#
Todas las lecciones de CSharp
Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A C#
Introducción Y Entorno
Creación De Proyecto C#
Introducción Y Entorno
Variables Y Constantes
Sintaxis
Tipos De Datos
Sintaxis
Operadores
Sintaxis
Control De Flujo
Sintaxis
Funciones
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Interpolación De Strings
Sintaxis
Estructuras De Control Condicional
Sintaxis
Manejo De Valores Nulos
Sintaxis
Clases Y Encapsulación
Programación Orientada A Objetos
Objetos
Programación Orientada A Objetos
Constructores Y Destructores
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Genéricos
Programación Orientada A Objetos
Métodos Virtuales Y Sobrecarga
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Propiedades Y Encapsulación
Programación Orientada A Objetos
Métodos De Extensión
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Clases Parciales
Programación Orientada A Objetos
Miembros Estáticos
Programación Orientada A Objetos
Tuplas Y Tipos Anónimos
Programación Orientada A Objetos
Arrays Y Listas
Colecciones Y Linq
Diccionarios
Colecciones Y Linq
Conjuntos, Colas Y Pilas
Colecciones Y Linq
Uso De Consultas Linq
Colecciones Y Linq
Linq Avanzado
Colecciones Y Linq
Colas Y Pilas
Colecciones Y Linq
Conjuntos
Colecciones Y Linq
Linq Básico
Colecciones Y Linq
Delegados Funcionales
Programación Funcional
Records
Programación Funcional
Expresiones Lambda
Programación Funcional
Linq Funcional
Programación Funcional
Fundamentos De La Programación Funcional
Programación Funcional
Pattern Matching
Programación Funcional
Testing Unitario Con Xunit
Testing
Excepciones
Excepciones
Delegados
Programación Asíncrona
Eventos
Programación Asíncrona
Lambdas
Programación Asíncrona
Uso De Async Y Await
Programación Asíncrona
Tareas
Programación Asíncrona
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis básica y variantes de las expresiones lambda en C#.
- Diferenciar entre expresiones lambda de expresión y de bloque.
- Utilizar expresiones lambda con delegados predefinidos como Action, Func y Predicate.
- Entender el concepto de captura de variables (closures) y su comportamiento en diferentes contextos.
- Reconocer las implicaciones de rendimiento y problemas comunes asociados a la captura de variables en lambdas.