Forzando Llamadas de Funciones Requeridas en C#

El Problema: Llamadas de Funciones No Verificadas

En C#, es bastante común crear funciones que devuelven un estado, ayudando a los desarrolladores a monitorear el éxito o el fracaso de las operaciones. Sin embargo, una trampa común es que algunos desarrolladores pueden ignorar completamente estos estados devueltos. Esto puede llevar a consecuencias no deseadas si no se ejecuta un manejo de errores adecuado.

Por ejemplo, considera una clase Status, que podría contener información sobre si una operación fue exitosa o si hubo problemas durante la ejecución:

Status MyFunction()
{
   if(...) // algo malo
     return new Status(false, "Algo salió mal");
   else
     return new Status(true, "OK");
}

La expectativa es que todos los llamadores de MyFunction verifiquen el estado devuelto:

Status myStatus = MyFunction();
if (!myStatus.IsOK())
   // manejarlo, mostrar un mensaje,...

No obstante, algunos llamadores podrían ignorar perezosamente la verificación del estado:

// Ignorando el estado devuelto
MyFunction(); // llama a la función e ignora el Status devuelto

¿Esto Puede Prevenirse?

Esto plantea una pregunta importante: ¿Cómo podemos garantizar que cada llamador verifique el estado devuelto? En C++, se podría utilizar un destructor para verificar el estado al final del ciclo de vida del objeto. Sin embargo, en C#, los destructores son generalmente mal vistos debido a la naturaleza no determinista del recolector de basura.

La Solución: Usando Delegados para Llamadas de Funciones Requeridas

Aunque C# carece de la capacidad para forzar que un valor de retorno de un método sea llamado o verificado, un enfoque innovador implica el uso de delegados. Esta técnica no solo devuelve un valor; en su lugar, requiere que el llamador procese el estado devuelto explícitamente a través de un mecanismo de callback.

Ejemplo de Implementación

Veamos una implementación simple utilizando delegados:

using System;

public class Example
{
    public class Toy
    {
        private bool inCupboard = false;
        public void Play() { Console.WriteLine("Jugando."); }
        public void PutAway() { inCupboard = true; }
        public bool IsInCupboard { get { return inCupboard; } }
    }

    public delegate void ToyUseCallback(Toy toy);

    public class Parent
    {
        public static void RequestToy(ToyUseCallback callback)
        {
            Toy toy = new Toy();
            callback(toy);
            if (!toy.IsInCupboard)
            {
                throw new Exception("¡No pusiste tu juguete en el armario!");
            }
        }
    }

    public class Child
    {
        public static void Play()
        {
            Parent.RequestToy(delegate(Toy toy)
            {
                toy.Play();
                // ¡Ups! ¡Olvidé guardar el juguete!
            });
        }
    }

    public static void Main()
    {
        Child.Play();
        Console.ReadLine();
    }
}

Explicación del Código

En este ejemplo, cuando se llama a Parent.RequestToy, espera una función de callback que realice operaciones con el objeto Toy. Después de ejecutar el callback, verifica si el juguete ha sido guardado:

  • Si el callback no llama a toy.PutAway(), se lanza una excepción. Esto asegura que el llamador debe ocuparse del Toy al final de su uso, forzando efectivamente que se realicen las operaciones de seguimiento necesarias.
  • Este método no garantiza que el llamador recordará verificar el estado, pero impone una estructura de tal manera que eleva la importancia de la operación.

Conclusión

Aunque puede no ser posible en C# forzar verificaciones en los valores devueltos tan rigurosamente como en C++, podemos aprovechar los delegados para crear un patrón que comunica efectivamente la necesidad de una acción adicional, mejorando la fiabilidad de nuestro código. Utilizar este patrón ayuda a asegurar un manejo adecuado de los resultados de las funciones, al tiempo que mantiene limpias y organizadas sus prácticas de programación.

Al implementar este enfoque, aseguramos que los desarrolladores que utilizan tu código no puedan eludir fácilmente verificaciones esenciales. Aunque puede parecer un poco poco convencional, sirve como una forma útil de crear una cultura de responsabilidad en el manejo de errores.