CSharp

Tutorial CSharp: Delegados funcionales

Aprende a usar delegados funcionales Func, Action y Predicate en C# para escribir código modular y flexible con programación funcional.

Aprende CSharp y certifícate

Func, Action, Predicate

Los delegados genéricos funcionales son componentes fundamentales en la programación funcional con C#. Estos tipos predefinidos simplifican el trabajo con funciones como valores, permitiéndonos escribir código más conciso y flexible. Vamos a explorar los tres delegados genéricos más utilizados: Func<>, Action<> y Predicate<>.

Func<>

El delegado Func<> representa una función que acepta parámetros y devuelve un valor. Su firma genérica es:

Func<T1, T2, ..., TResult>

Donde:

  • T1, T2, ... son los tipos de los parámetros de entrada (hasta 16)
  • TResult es el tipo del valor de retorno

Un ejemplo básico de uso de Func<>:

// Func que recibe un int y devuelve un string
Func<int, string> convertirATexto = numero => $"El número es: {numero}";

// Uso del delegado
string resultado = convertirATexto(42);
Console.WriteLine(resultado); // Imprime: "El número es: 42"

También podemos crear delegados Func<> con múltiples parámetros:

// Func que recibe dos enteros y devuelve su suma
Func<int, int, int> sumar = (a, b) => a + b;

int total = sumar(5, 3);
Console.WriteLine(total); // Imprime: 8

Si solo necesitamos un valor de retorno sin parámetros, usamos Func<TResult>:

// Func sin parámetros que devuelve un valor aleatorio
Func<double> obtenerAleatorio = () => new Random().NextDouble();

double valorAleatorio = obtenerAleatorio();
Console.WriteLine(valorAleatorio); // Imprime un número aleatorio entre 0 y 1

Action<>

El delegado Action<> representa un método void que acepta parámetros pero no devuelve ningún valor. Su firma genérica es:

Action<T1, T2, ...>

Donde T1, T2, ... son los tipos de los parámetros de entrada (hasta 16).

Ejemplo básico de Action<>:

// Action que recibe un string y lo imprime
Action<string> mostrarMensaje = mensaje => Console.WriteLine(mensaje);

// Uso del delegado
mostrarMensaje("Hola mundo"); // Imprime: "Hola mundo"

Para operaciones que no requieren parámetros, usamos simplemente Action:

// Action sin parámetros
Action saludar = () => Console.WriteLine("¡Hola a todos!");

saludar(); // Imprime: "¡Hola a todos!"

Un ejemplo con múltiples parámetros:

// Action con dos parámetros
Action<string, int> repetirMensaje = (mensaje, veces) => {
    for (int i = 0; i < veces; i++)
        Console.WriteLine(mensaje);
};

repetirMensaje("Eco", 3);
// Imprime:
// Eco
// Eco
// Eco

Predicate<>

El delegado Predicate<> es un caso especial que representa un método que evalúa una condición y devuelve un booleano. Es equivalente a Func<T, bool>. Su firma es:

Predicate<T>

Donde T es el tipo del parámetro que se evalúa.

Ejemplo básico de Predicate<>:

// Predicate que verifica si un número es par
Predicate<int> esPar = numero => numero % 2 == 0;

// Uso del delegado
bool resultado = esPar(42);
Console.WriteLine(resultado); // Imprime: True

resultado = esPar(7);
Console.WriteLine(resultado); // Imprime: False

Los predicados son especialmente útiles con métodos de colecciones como FindAll o RemoveAll:

List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Encontrar todos los números pares
List<int> pares = numeros.FindAll(esPar);

// Mostrar los números pares
foreach (int numero in pares)
    Console.Write($"{numero} "); // Imprime: 2 4 6 8 10

Uso práctico de los delegados funcionales

Estos delegados son particularmente útiles cuando necesitamos:

  • Pasar comportamiento como parámetro a métodos
  • Almacenar funciones en variables o estructuras de datos
  • Crear código más flexible que pueda adaptarse a diferentes situaciones

Veamos un ejemplo práctico que combina los tres tipos de delegados:

public class Procesador
{
    // Método que utiliza un Func para transformar datos
    public List<TResult> Transformar<T, TResult>(List<T> items, Func<T, TResult> transformador)
    {
        List<TResult> resultados = new List<TResult>();
        foreach (T item in items)
            resultados.Add(transformador(item));
        return resultados;
    }
    
    // Método que utiliza un Predicate para filtrar datos
    public List<T> Filtrar<T>(List<T> items, Predicate<T> condicion)
    {
        return items.FindAll(condicion);
    }
    
    // Método que utiliza un Action para procesar datos
    public void Procesar<T>(List<T> items, Action<T> procesador)
    {
        foreach (T item in items)
            procesador(item);
    }
}

Y así podríamos utilizarlo:

var procesador = new Procesador();
List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };

// Usar Func para duplicar cada número
List<int> duplicados = procesador.Transformar(numeros, n => n * 2);
// duplicados contiene: 2, 4, 6, 8, 10

// Usar Predicate para obtener solo números mayores que 3
List<int> mayoresQueTres = procesador.Filtrar(numeros, n => n > 3);
// mayoresQueTres contiene: 4, 5

// Usar Action para imprimir cada número
procesador.Procesar(numeros, n => Console.WriteLine($"Número: {n}"));
// Imprime cada número en la lista

Los delegados funcionales nos permiten escribir código más modular y reutilizable, facilitando la implementación de patrones de programación funcional en C#. Al dominar estos tipos, podemos crear soluciones más elegantes y flexibles para una amplia variedad de problemas de programación.

Composición de funciones

La composición de funciones es una técnica fundamental en la programación funcional que nos permite combinar múltiples funciones para crear nuevas funcionalidades más complejas. En C#, gracias a los delegados funcionales, podemos implementar este concepto de manera elegante y efectiva.

La idea básica de la composición es tomar la salida de una función y utilizarla como entrada de otra. Si tenemos dos funciones f(x) y g(x), la composición se expresa como (f ∘ g)(x) = f(g(x)).

Implementación básica de composición

Podemos crear una función que componga dos delegados Func<>:

// Método para componer dos funciones
public static Func<T1, T3> Componer<T1, T2, T3>(Func<T2, T3> f, Func<T1, T2> g)
{
    return x => f(g(x));
}

Veamos un ejemplo sencillo:

// Definimos dos funciones simples
Func<int, int> duplicar = x => x * 2;
Func<int, string> convertirATexto = x => $"El valor es: {x}";

// Componemos las funciones
Func<int, string> duplicarYConvertir = Componer(convertirATexto, duplicar);

// Usamos la función compuesta
string resultado = duplicarYConvertir(5);
Console.WriteLine(resultado); // Imprime: "El valor es: 10"

Encadenamiento de funciones

Podemos extender este concepto para encadenar múltiples funciones:

// Definimos varias funciones
Func<int, int> sumarUno = x => x + 1;
Func<int, int> multiplicarPorTres = x => x * 3;
Func<int, string> formatear = x => $"Resultado: {x}";

// Componemos múltiples funciones
var operacionCompleja = Componer(formatear, 
                        Componer(multiplicarPorTres, sumarUno));

// Aplicamos la composición
Console.WriteLine(operacionCompleja(5)); // Imprime: "Resultado: 18"
// (5 + 1) * 3 = 18

Métodos de extensión para composición

Para hacer más elegante la composición, podemos crear métodos de extensión:

public static class FuncExtensions
{
    public static Func<T1, T3> ComposeWith<T1, T2, T3>(
        this Func<T1, T2> g, 
        Func<T2, T3> f)
    {
        return x => f(g(x));
    }
}

Esto nos permite encadenar funciones de manera más legible:

// Usando el método de extensión
Func<int, int> duplicar = x => x * 2;
Func<int, int> sumarTres = x => x + 3;
Func<int, string> convertir = x => x.ToString();

// Composición encadenada
var pipeline = duplicar
    .ComposeWith(sumarTres)
    .ComposeWith(convertir);

Console.WriteLine(pipeline(5)); // Imprime: "13"
// (5 * 2) + 3 = 13

Composición con Action y Predicate

También podemos componer otros tipos de delegados:

// Componer un Predicate con una transformación previa
public static Predicate<T1> ComponerPredicate<T1, T2>(
    Predicate<T2> predicado, 
    Func<T1, T2> transformacion)
{
    return x => predicado(transformacion(x));
}

// Componer Actions secuencialmente
public static Action<T> ComponerActions<T>(Action<T> primeraAccion, Action<T> segundaAccion)
{
    return x => {
        primeraAccion(x);
        segundaAccion(x);
    };
}

Ejemplo de uso:

// Composición con Predicate
Func<string, int> obtenerLongitud = s => s.Length;
Predicate<int> esPar = n => n % 2 == 0;

// Componer: verificar si la longitud de un string es par
Predicate<string> longitudEsPar = ComponerPredicate(esPar, obtenerLongitud);

Console.WriteLine(longitudEsPar("Hola"));    // False (4 es par)
Console.WriteLine(longitudEsPar("Mundo"));   // False (5 es impar)

// Composición con Actions
Action<string> registrar = s => Console.WriteLine($"Procesando: {s}");
Action<string> mostrar = s => Console.WriteLine($"Resultado: {s.ToUpper()}");

// Componer acciones secuenciales
Action<string> procesarYMostrar = ComponerActions(registrar, mostrar);

procesarYMostrar("ejemplo");
// Imprime:
// Procesando: ejemplo
// Resultado: EJEMPLO

Aplicaciones prácticas

La composición de funciones es especialmente útil para:

  • Procesamiento de datos en pipeline: transformar datos a través de una serie de pasos.
  • Separación de responsabilidades: dividir operaciones complejas en funciones más simples.
  • Reutilización de código: combinar funciones existentes para crear nuevas funcionalidades.

Veamos un ejemplo práctico de procesamiento de datos:

// Funciones para procesar datos de usuario
Func<string, string> normalizar = nombre => nombre.Trim().ToLower();
Func<string, string> capitalizar = nombre => 
    string.Join(" ", nombre.Split(' ')
    .Select(parte => char.ToUpper(parte[0]) + parte.Substring(1)));
Func<string, string> formatear = nombre => $"Usuario: {nombre}";

// Componemos las funciones para crear un pipeline de procesamiento
var procesarNombre = normalizar
    .ComposeWith(capitalizar)
    .ComposeWith(formatear);

// Aplicamos el pipeline a diferentes entradas
string[] nombres = { "  juan pérez  ", "MARÍA LÓPEZ", "carlos  RODRÍGUEZ" };

foreach (var nombre in nombres)
{
    Console.WriteLine(procesarNombre(nombre));
}
// Imprime:
// Usuario: Juan Pérez
// Usuario: María López
// Usuario: Carlos Rodríguez

Composición parcial y currificación

La currificación (convertir una función de múltiples argumentos en una secuencia de funciones de un solo argumento) facilita la composición parcial:

// Función currificada para sumar
Func<int, Func<int, int>> sumar = a => b => a + b;

// Creamos una función parcial
Func<int, int> sumar5 = sumar(5);

// Componemos con otra función
var duplicarYSumar5 = Componer(sumar5, duplicar);

Console.WriteLine(duplicarYSumar5(3)); // Imprime: 11
// (3 * 2) + 5 = 11

La composición de funciones nos permite construir soluciones modulares y flexibles a partir de componentes simples. Esta técnica es fundamental en la programación funcional y nos ayuda a escribir código más limpio, mantenible y reutilizable en C#.

Callbacks y estrategias

Los callbacks y las estrategias son patrones de diseño que aprovechan los delegados funcionales para crear código más flexible y desacoplado. Estos patrones permiten que un componente ejecute código proporcionado por otro componente, sin necesidad de conocer los detalles de implementación.

Callbacks en C#

Un callback es una función que se pasa como argumento a otra función, permitiendo que esta última la invoque cuando sea necesario. En C#, los delegados son el mecanismo perfecto para implementar callbacks.

public class Procesador
{
    public void ProcesarDatos(string[] datos, Action<string> callback)
    {
        foreach (var dato in datos)
        {
            // Realizar algún procesamiento
            string resultado = dato.ToUpper();
            
            // Invocar el callback con el resultado
            callback(resultado);
        }
    }
}

Este patrón es especialmente útil para operaciones asíncronas o de larga duración:

// Uso del callback
var procesador = new Procesador();
string[] nombres = { "ana", "juan", "maría" };

procesador.ProcesarDatos(nombres, nombreProcesado => {
    Console.WriteLine($"Dato procesado: {nombreProcesado}");
});

// Imprime:
// Dato procesado: ANA
// Dato procesado: JUAN
// Dato procesado: MARÍA

Los callbacks también pueden utilizarse para notificar cuando una operación ha finalizado:

public void RealizarOperacionLarga(Action alCompletar)
{
    // Simulamos una operación que toma tiempo
    Thread.Sleep(2000);
    
    // Notificamos que la operación ha terminado
    alCompletar();
}

// Uso:
Console.WriteLine("Iniciando operación...");

RealizarOperacionLarga(() => {
    Console.WriteLine("¡Operación completada!");
});

Callbacks con resultados

Cuando necesitamos que el callback devuelva un resultado, utilizamos Func<> en lugar de Action<>:

public void ProcesarConResultado(int valor, Func<int, string> transformador)
{
    string resultado = transformador(valor);
    Console.WriteLine($"Resultado: {resultado}");
}

// Uso:
ProcesarConResultado(42, numero => $"El valor transformado es: {numero * 2}");
// Imprime: Resultado: El valor transformado es: 84

Patrón de estrategia

El patrón de estrategia permite seleccionar un algoritmo en tiempo de ejecución. En lugar de implementar un único algoritmo directamente, el código recibe instrucciones en runtime sobre qué algoritmo usar.

Una implementación básica utilizando delegados funcionales:

public class Calculadora
{
    public int Calcular(int a, int b, Func<int, int, int> estrategia)
    {
        return estrategia(a, b);
    }
}

Esta implementación permite cambiar fácilmente la operación matemática:

var calc = new Calculadora();

// Definimos diferentes estrategias
Func<int, int, int> sumar = (x, y) => x + y;
Func<int, int, int> restar = (x, y) => x - y;
Func<int, int, int> multiplicar = (x, y) => x * y;
Func<int, int, int> dividir = (x, y) => y != 0 ? x / y : 0;

// Usamos diferentes estrategias según necesitemos
Console.WriteLine(calc.Calcular(10, 5, sumar));        // 15
Console.WriteLine(calc.Calcular(10, 5, restar));       // 5
Console.WriteLine(calc.Calcular(10, 5, multiplicar));  // 50
Console.WriteLine(calc.Calcular(10, 5, dividir));      // 2

Estrategias con predicados

Podemos utilizar Predicate<> para implementar estrategias de filtrado:

public class Filtrador<T>
{
    public List<T> Filtrar(List<T> items, Predicate<T> estrategia)
    {
        return items.FindAll(estrategia);
    }
}

Ejemplo de uso con diferentes estrategias de filtrado:

var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var filtrador = new Filtrador<int>();

// Diferentes estrategias de filtrado
Predicate<int> pares = n => n % 2 == 0;
Predicate<int> impares = n => n % 2 != 0;
Predicate<int> mayoresQueCinco = n => n > 5;

// Aplicamos diferentes estrategias
var numerosPares = filtrador.Filtrar(numeros, pares);
var numerosImpares = filtrador.Filtrar(numeros, impares);
var numerosMayores = filtrador.Filtrar(numeros, mayoresQueCinco);

// Mostramos resultados
Console.WriteLine($"Pares: {string.Join(", ", numerosPares)}");
Console.WriteLine($"Impares: {string.Join(", ", numerosImpares)}");
Console.WriteLine($"Mayores que 5: {string.Join(", ", numerosMayores)}");

// Imprime:
// Pares: 2, 4, 6, 8, 10
// Impares: 1, 3, 5, 7, 9
// Mayores que 5: 6, 7, 8, 9, 10

Combinando callbacks y estrategias

Podemos combinar ambos patrones para crear soluciones más potentes:

public class Procesador<T>
{
    public void Procesar(
        List<T> items, 
        Predicate<T> filtro, 
        Func<T, T> transformador, 
        Action<T> callback)
    {
        foreach (var item in items)
        {
            // Aplicamos la estrategia de filtrado
            if (filtro(item))
            {
                // Aplicamos la estrategia de transformación
                T resultado = transformador(item);
                
                // Invocamos el callback con el resultado
                callback(resultado);
            }
        }
    }
}

Ejemplo de uso:

var procesador = new Procesador<int>();
var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Procesamos solo los números pares, los duplicamos y mostramos el resultado
procesador.Procesar(
    numeros,
    n => n % 2 == 0,                // Estrategia de filtrado: solo pares
    n => n * 2,                     // Estrategia de transformación: duplicar
    n => Console.WriteLine($"Resultado: {n}")  // Callback: mostrar resultado
);

// Imprime:
// Resultado: 4
// Resultado: 8
// Resultado: 12
// Resultado: 16
// Resultado: 20

Caso práctico: Sistema de notificaciones

Veamos un ejemplo más completo de un sistema de notificaciones que utiliza callbacks y estrategias:

public class SistemaNotificaciones
{
    // Método que utiliza una estrategia para filtrar mensajes
    // y un callback para enviar notificaciones
    public void EnviarNotificaciones(
        List<string> mensajes,
        Predicate<string> filtroMensajes,
        Action<string> notificador)
    {
        foreach (var mensaje in mensajes)
        {
            if (filtroMensajes(mensaje))
            {
                notificador(mensaje);
            }
        }
    }
}

Implementación con diferentes estrategias y callbacks:

var sistema = new SistemaNotificaciones();
var mensajes = new List<string> {
    "ERROR: Sistema caído",
    "INFO: Operación completada",
    "WARN: Espacio en disco bajo",
    "ERROR: Conexión perdida",
    "INFO: Nuevo usuario registrado"
};

// Estrategias de filtrado
Predicate<string> soloErrores = m => m.StartsWith("ERROR");
Predicate<string> soloInfo = m => m.StartsWith("INFO");

// Callbacks de notificación
Action<string> notificarConsola = m => Console.WriteLine($"CONSOLA: {m}");
Action<string> notificarEmail = m => Console.WriteLine($"EMAIL: {m}");
Action<string> notificarSMS = m => Console.WriteLine($"SMS: {m}");

// Enviamos errores por email y SMS
Console.WriteLine("=== Notificando errores ===");
sistema.EnviarNotificaciones(mensajes, soloErrores, notificarEmail);
sistema.EnviarNotificaciones(mensajes, soloErrores, notificarSMS);

// Enviamos información solo por consola
Console.WriteLine("\n=== Notificando información ===");
sistema.EnviarNotificaciones(mensajes, soloInfo, notificarConsola);

Ventajas de usar callbacks y estrategias

Estos patrones ofrecen numerosos beneficios:

  • Flexibilidad: Permiten cambiar el comportamiento sin modificar el código existente.
  • Desacoplamiento: Reducen las dependencias entre componentes.
  • Reutilización: Facilitan el uso del mismo código con diferentes comportamientos.
  • Testabilidad: Simplifican las pruebas al permitir inyectar comportamientos simulados.

Los callbacks y las estrategias son herramientas fundamentales en la programación funcional con C#, permitiéndonos crear código más modular, extensible y mantenible. Al combinarlos con los delegados funcionales (Func<>, Action<> y Predicate<>), podemos implementar soluciones elegantes para una amplia variedad de problemas de programación.

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 Delegados funcionales

Evalúa tus conocimientos de esta lección Delegados funcionales 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 los delegados genéricos Func<>, Action<> y Predicate<> y sus usos.
  • Aprender a componer funciones para crear pipelines de procesamiento.
  • Implementar métodos de extensión para facilitar la composición de funciones.
  • Aplicar patrones de diseño como callbacks y estrategia usando delegados funcionales.
  • Desarrollar código modular, flexible y reutilizable mediante programación funcional en C#.