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ícate

Sintaxis 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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

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.

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

Accede GRATIS a CSharp y certifícate

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.