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ícateFunc, 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.
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.
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 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#.