Devolver null en una función es una mala práctica, vamos a ver en este artículo porque es una mala práctica y como podemos evitarlo.
Devolver null es una mala práctica porque estamos obligando al llamador a verificar si el objeto devuelto es null antes de realizar cualquier acción. Vamos a ver casos concretos.
Cuando se devuelve una colección
Fijaros en el siguiente código, tenemos una función que devuelve una lista, si no encuentra datos a devolver el valor devuelto es null.public List<User> GetUsers() { if (...no hay usuarios...) return null; }
Esta situación obliga al llamador a verificar si es null la lista antes de hacer nada.
List<User> users = GetUsers() if (users != null) { foreach (User user in users) { ....hace lo que sea.... } }
Ensuciando el código, imagina if anidados con llamadas a funciones que pueden devolver null, el código quedaria bastante terrible.
Una mejor solución es en caso de no tener datos que devolver, crear una lista vacía como valor de retorno
public List<User> GetUsers() { if (...no hay usuarios...) return new List<User>(); }
Ahora el código del llamador se simplifica
List<User> users = GetUsers() foreach (User user in users) { ....hace lo que sea.... }
Quedando más sencillo.
Cuandose devuelve un objeto
Otro escenario es cuando tenemos una función que devuelve un objeto pero en función de cada caso la solución puede ser diferente.Lanzar una excepción
Por ejemplo el siguiente código, tenemos una función que devuelve una objeto, si no encuentra el objeto a devolver, el valor devuelto es null.public User GetUserById(int id) { if (...no existe ese usuario...) return null; }
Volvemos a tener que verificar si es null el resultado de la función antes de hacer nada.
User user = repository.GetUserById(id) if (user != null) { ....hace lo que sea.... }
Volvemos a ensuciar el código y además si nos olvidamos de validar algún null, es facil caer en un NullReferenceException.
En este escenario lo más apropiado es lanzar una excepción si no existe ningún usuario con ese identificador
public User GetUserById(int id) { if (...no existe ese usuario...) throw new Exception("User not found with id "+ id); }
Ahora el código del llamador se simplifica
User user = repository.GetUserById(id) ....hace lo que sea....
En la pila de llamadas debería existir en las capas más altas alguien manejando esta posible excepción, pero en las partes intermedias el código se simplifica al no tener que validar nulos.
Utilizar Null Object Pattern
Hay otros escenariós ante una situación parecida, donde la solución más apropiada pueda ser utilizar el patrón Null Object.Imaginemos que tenemos una factoría de logger
public Logger GetLogger(string type) { if (...no existe ese tipo...) return null; }
Mismo problema en el llamador, hay que verificar que el logger no sea nulo antes de hacer nada.
Logger logger = factory.GetLogger(type) . . . if (logger != null) { logger.Debug("escribimos traza"); }
Volvemos a ensuciar el código, en este caso podemos mejorar el código creando una función Debug que verifica si logger es null antes de invocar el método Debug del logger, pero una mejor solución es utilizar Null Object Pattern.
Abstraemos el logger con una interfaz, y tenemos la implementación real y otra vacía.
public ILogger { void Debug(string message); } public RealLogger:ILogger public void Debug(string message) { //hace lo que sea } } public NullLogger:ILogger { public void Debug(string message) { //No hace nada } }
Ahora la factoría en lugar de devolver null devuelve NullLogger si no existe el tipo.
public Logger GetLogger(string type) { if (...no existe ese tipo...) return new NullLogger(); }
Ahora cada vez que utilicemos logger no es necesario verificar si es null.
Logger logger = factory.GetLogger(type) . . . logger.Debug("escribimos traza");
Un poco de historia: Sir Charles Antony Richard Hoare (aka C.A.R. Hoare) fue el primero que introdujo la referencia nula, os dejo unas palabras suyas donde reconoce que fue un error.
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. In recent years, a number of program analysers like PREfix and PREfast in Microsoft have been used to check references, and give warnings if there is a risk they may be non-null. More recent programming languages like Spec# have introduced declarations for non-null references. This is the solution, which I rejected in 1965.
Y para terminar un poco de actualidad: los lenguajes funcionles como pueden ser F# y Kotlin por defecto no permiten asignar null para evitar este tipo de situaciones.
Interesante el articulo y la parte de Un poco e historia.
ResponderEliminarSaludos
Muchas gracias por el comentario Victor.
EliminarSaludos para ti también.
El problema no solo es el devolver null, sino el cómo representas el resultado de la operación. Por ejemplo, en el caso de la lista, si ocurrió un fallo en la llamada a la BDD ¿qué regresas? no puedes regresar una lista vacía, puesto que una lista vacía significa que no encontraste datos, pero no que ocurrió un error. Lo mismo ocurre con un elemento simple, si regresas null quiere decir que no se encontró en la BDD, pero si ocurrió un error en la llamada, ¿regresas null también?.
ResponderEliminarUna solución es devolver una tupla, uno de los valores sería bool y el otro la lista, el bool sería verdadero si no ocurrió ningún error, y falso en caso de error, y la lista estaría vacía si ocurrió un error o si no ocurrió ningún error pero no encontró resultados.
Pero desde mi punto de vista es mejor crear una clase que pueda representar los dos estados (error/no error) y también el resultado, y definir métodos para acceder fácilmente al resultado de la búsqueda.