En este artículo quiero tratar como se puede usar la sentencia return para asegurar succesivamente la calidad de datos y obtener un código más limpio.

El problema

La programación estructurada se inventó para evitar el comando goto. La sentencia return también es una especie de goto ya que salta fuera de la subrutina aunque todavía queda código detrás a ejecutar. Por eso hay guías de estilo que piden que una función sólo puede tener una sentencia de return al final – normalmente para devolver el resultado de la función.

Esto es una buena regla en un cálculo lineal, es decir, cuando no se necesitan comprobar errores antes de seguir. Cálculos matemáticos son muchas veces de este tipo. La suma de a y b no falla aunque las variables contengan datos inválidos.

No obstante, en la mayoría de los casos necesitamos la comprobación de errores. En un escenario real el tratamiento de errores ocupa a menudo más espacio que el cálculo principal.

Veamos un ejemplo. Queremos leer algunos bytes de un fichero. Un programa simple tendría pocas líneas.

char buffer[1000];                       // Un buffer
FILE* f = fopen("mi_fichero.txt", "r");  // Abre fichero
fread(buffer, 1, sizeof(buffer), f);     // Lee algo
fclose(f);                               // Cierra fichero

Si tenemos en cuenta los posibles errores, la misma funcionalidad se extendería así:

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre fichero
FILE* f = fopen("mi_fichero.txt", "r");  

// Si podía abrir el fichero
if (f != NULL)  
{
	// Entonces lee del fichero
    const size_t read_result = 
		fread(buffer, 1, sizeof(buffer), f);
		
	// Si podía leer el fichero
    if (read_result == sizeof(buffer) 
	{
		// Haz algo
	}
	else
    {
		// Marca error si no podía leer del fichero
        error = true; 
    }
	
	// En todo caso cierra el fichero
    fclose(f);
}
else
{
	// Marca error si no podía abrir el fichero
    error = true;
}

Para cada comprobación que necesitamos para seguir adelante debemos añadir un if anidado. Esto ensucia tanto el código que podemos perder la vista sobre la esencia de la función. La idea principal de abrir, leer y cerrar un fichero queda obfuscada por tanta comprobación de error. Y este ejemplo es todavía bastante simple en comparación con un código real.

Usar múltiples return

Si permitimos el uso de varios return en medio, entonces podemos reducir el número de if anidados.

char buffer[1000];   // Un buffer
bool error = false;  // Un marcador de error

// Abre el fichero
FILE* f = fopen("mi_fichero.txt", "r");
if (f == NULL)
{
    return;
}

// Lee el fichero
const size_t read_result = 
	fread(buffer, 1, sizeof(buffer), f);
if (read_result != sizeof(buffer) 
{
    error = true;
}

// Cierra el fichero
fclose(f);

La filosofía de este procedimiento es consultar en cada paso, si los datos obtenidos son buenos y me permiten continuar. Si no puedo continuar, entonces termina la ejecución. Por ejemplo, si no encuentro el fichero en fopen, entonces no hay nada que leer y debo cancelar la función. Ya no tengo el problema de if anidados y puedo fácilmente añadir una condición más si hace falta.

Los chequeos con return en medio ayudan también a depurar el código. Si la ejecución del programa ha llegado a un cierto punto dentro de una función, entonces ya sé que todos los datos que se hayan obtenido anteriormente fueron buenos. La estructura del código me permite concentrar en cada paso del algoritmo sin dejar de hacer un chequeo exhaustivo de posibles errores.

Un return en medio de la función devuelve típicamente un código de error. Un return que devuevle un código de error notifica una excepción, es decir, un error no deseado. Por lo tanto suele ser posible lanzar una excepción en lugar de devolver un código de error. Cual de las dos formas es preferible es una decisión del diseño del programa a alto nivel y no a nivel de función. Hay lenguajes como C en que no se pueden lanzar excepciones. En otros lenguajes como Java el uso de excepciones es lo habitual.

Por cierto, para salir o saltar en bucles suele existir una sentencia equivalente al return. En los lenguaje C y Java se llaman break y continue, respectivamente. Son menos usado que el return y probablemente con razón. El enunciado return sale de la función sin más buscar. En cambio, las interrupciones de bucles saltan al inicio o al final del bucle que a veces cuesta encontrar – sobre todo en funciones largas y con bucles anidados.

Una solución curiosa

En este apartado presento una idea un poco peculiar. No recomiendo usarla y la presento sólo como una curiosidad.

La idea consiste en encadenar todas las comprobaciones en un sólo if y hacer de paso todo el trabajo.

char buffer[1000];
bool error = true;
FILE* f;
if (f = fopen("mi_fichero.txt", "r") &&
    sizeof(buffer) == fread(buffer, 1, sizeof(buffer), f))
{
    error = false;
}
if (f)
{
    fclose(f);
}

Esta construcción funciona así: se ejecuta fopen y se asigna el puntero que devuelve a f. El valor de una asignación es el valor asignado, es decir el puntero devuelto. Si este puntero vale NULL, entonces se interpreta como un false booleano. En este caso ya no se evalúa el segundo operador de la expresión && y el programa no entra en el if. En el caso que el puntero f no es nulo, se ejecuta fread y se compara con el valor esperado. Si no lo es, entonces no se entra en el if.

La condición del if sólo es cierta si todos los operadores de la expresión devuelven valores no nulos. Al primer valor nulo, se termina la evaluación de la expresión booleana y no se ejecutan las funciones siguientes. Esta forma compacta bastante el código sin dejar de comprobar errores.

Este ejemplo demuestra que alguien entiende mucho de las caracterísicas del lenguaje de programación pero quizá no tanto de la semántica del código. Muchos lenguajes como C y Java permiten asignaciones en expresiones booleanas aunque es mala práctica, ya que una expresión booleana, por semántica, es un valor y no un contenedor de datos. En cambio, este ejemplo no sólo asigna el puntero de fichero sino pone todo el código ejecutable en la condición del if.

En lenguaje humano, un uso semánticamente correcto del if sería algo como “Si lo puede hacer(condición), entonces lo hago (bloque condicional)”. El ejemplo arriba es más bien algo como “si lo hago todo, entonces bien”.

Conclusión

Hemos visto que la comprobación de errores puede afectar bastante a la claridad del código sobre el fin principal de una función. Dejando aparte la filosofía de una programación estructurada estricta podemos conseguir a reducir el nivel de anidamiento y separar el código en los pasos principales del algoritmo.

Referencias

El ejemplo de la “solución curiosa” fue inspirado en el código fuente de una implementación del algoritmo BSDIFF. Ver, por ejemplo, la función main del fichero bsdiff.cpp, líneas 260 a 264. Para descargar el archivo zip, pincha aquí.

Lectura adicional

Otras palabras claves
Técnicas el programación
Código más bonito