¿Deben las funciones devolver nulo o un objeto vacío?

¿Cuál es la mejor práctica al devolver datos de funciones? ¿Es mejor devolver un nulo o un objeto vacío? ¿Y por qué debería uno hacer uno sobre el otro?

Considera esto:

public UserEntity GetUserById(Guid userId) { //Imagine some code here to access database..... //Check if data was returned and return a null if none found if (!DataExists) return null; //Should I be doing this here instead? //return new UserEntity(); else return existingUserEntity; } 

Supongamos que habría casos válidos en este progtwig que no incluirían información de usuario en la base de datos con ese GUID. ¿Me imagino que no sería apropiado lanzar una excepción en este caso? También tengo la impresión de que el manejo de excepciones puede perjudicar el rendimiento.

Normalmente, devolver la nulidad es la mejor idea si tiene la intención de indicar que no hay datos disponibles.

Un objeto vacío implica que se han devuelto datos, mientras que devolver el nulo indica claramente que no se ha devuelto nada.

Además, devolver un nulo dará como resultado una excepción nula si intenta acceder a los miembros en el objeto, lo que puede ser útil para resaltar el código incorrecto. Intentar acceder a un miembro sin nada no tiene sentido. El acceso a los miembros de un objeto vacío no fallará, lo que significa que los errores no se descubrirán.

Depende de lo que tenga más sentido para su caso.

¿Tiene sentido devolver nulo, por ejemplo, “no existe tal usuario”?

¿O tiene sentido crear un usuario predeterminado? Esto tiene más sentido cuando puede suponerse con seguridad que si un usuario NO existe, el código de llamada pretende que exista uno cuando lo solicite.

¿O tiene sentido lanzar una excepción (a la “FileNotFound”) si el código de llamada exige un usuario con una identificación no válida?

Sin embargo, desde una separación de las preocupaciones / punto de vista de SRP, las dos primeras son más correctas. Y técnicamente el primero es el más correcto (pero solo por un pelo) – GetUserById solo debería ser responsable de una cosa: conseguir al usuario. Manejar su propio caso de “usuario no existe” al devolver algo más podría ser una violación de SRP. Separar en una verificación diferente – bool DoesUserExist(id) sería apropiado si elige arrojar una excepción.

Basado en extensos comentarios a continuación : si se trata de una pregunta de diseño de nivel de API, este método podría ser análogo a “OpenFile” o “ReadEntireFile”. Estamos “abriendo” un usuario de un repository e hidratando el objeto de los datos resultantes. Una excepción podría ser apropiada en este caso. Puede que no lo sea, pero podría ser.

Todos los enfoques son aceptables, simplemente depende del contexto más amplio de la API / aplicación.

Personalmente, uso NULL. Deja en claro que no hay datos para regresar. Pero hay casos en que un objeto nulo puede ser útil.

Si su tipo de devolución es una matriz, devuelva una matriz vacía; de lo contrario, devuelva null.

Debe lanzar una excepción (solo) si un contrato específico está roto.
En su ejemplo específico, al solicitar una UserEntity basada en un Id conocido, dependerá del hecho de que los usuarios que faltan (eliminados) sean un caso esperado. Si es así, devuelva null pero si no es un caso esperado, genere una excepción.
Tenga en cuenta que si la función se llama UserEntity GetUserByName(string name) , probablemente no lanzará, pero devolverá nulo. En ambos casos, devolver una UserEntity vacía no sería útil.

Para cadenas, matrices y colecciones, la situación suele ser diferente. Recuerdo alguna directriz de MS que los métodos deberían aceptar null como una lista ‘vacía’ pero devolver colecciones de longitud cero en lugar de null . Lo mismo para cadenas. Tenga en cuenta que puede declarar matrices vacías: int[] arr = new int[0];

Esta es una pregunta comercial, dependiendo de si la existencia de un usuario con una ID de guía específica es un caso de uso normal esperado para esta función, o es una anomalía que impedirá que la aplicación complete con éxito cualquier función que este método proporcione al usuario oponerse a…

Si se trata de una “excepción”, la ausencia de un usuario con ese ID evitará que la aplicación complete con éxito cualquier función que esté realizando (supongamos que estamos creando una factura para un cliente al que le enviamos el producto). ), entonces esta situación debería arrojar una ArgumentException (u otra excepción personalizada).

Si un usuario faltante está bien, (uno de los posibles resultados normales de llamar a esta función), entonces devuelve un valor nulo …

EDITAR: (para abordar el comentario de Adam en otra respuesta)

Si la aplicación contiene múltiples procesos comerciales, uno o más de los cuales requieren un Usuario para completarse exitosamente, y uno o más de los cuales pueden completarse exitosamente sin un usuario, entonces la excepción debe ser lanzada más arriba en la stack de llamadas, más cerca de donde los procesos de negocio que requieren un usuario están llamando a este hilo de ejecución. Los métodos entre este método y ese punto (donde se lanza la excepción) deberían simplemente comunicar que no existe ningún usuario (nulo, booleano, lo que sea, esto es un detalle de implementación).

Pero si todos los procesos dentro de la aplicación requieren un usuario, aún lanzaría la excepción en este método …

Yo personalmente devolvería nulo, porque así es como esperaría que actuara la capa DAL / Repositorio.

Si no existe, no devuelva nada que pueda interpretarse como la obtención de un objeto con éxito, null funciona muy bien aquí.

Lo más importante es ser consistente en toda su capa DAL / Repos, de esa manera no se confunde sobre cómo usarla.

tiendo a

  • return null si la Id. de objeto no existe cuando no se sabe de antemano si debería existir.
  • throw si la identificación del objeto no existe cuando debería existir.

Diferenciaré estos dos escenarios con estos tres tipos de métodos. Primero:

 Boolean TryGetSomeObjectById(Int32 id, out SomeObject o) { if (InternalIdExists(id)) { o = InternalGetSomeObject(id); return true; } else { return false; } } 

Segundo:

 SomeObject FindSomeObjectById(Int32 id) { SomeObject o; return TryGetObjectById(id, out o) ? o : null; } 

Tercero:

 SomeObject GetSomeObjectById(Int32 id) { SomeObject o; if (!TryGetObjectById(id, out o)) { throw new SomeAppropriateException(); } return o; } 

Otro enfoque más implica pasar un objeto de callback o un delegado que operará en el valor. Si no se encuentra un valor, no se llama a la callback.

 public void GetUserById(Guid id, UserCallback callback) { // Lookup user if (userFound) callback(userEntity); // or callback.Call(userEntity); } 

Esto funciona bien cuando quiere evitar verificaciones nulas en todo su código, y cuando no encuentra un valor no es un error. También puede proporcionar una callback para cuando no se encuentren objetos si necesita algún procesamiento especial.

 public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound) { // Lookup user if (userFound) callback(userEntity); // or callback.Call(userEntity); else notFound(); // or notFound.Call(); } 

El mismo enfoque con un solo objeto podría ser similar a:

 public void GetUserById(Guid id, UserCallback callback) { // Lookup user if (userFound) callback.Found(userEntity); else callback.NotFound(); } 

Desde una perspectiva de diseño, realmente me gusta este enfoque, pero tiene la desventaja de hacer que el sitio de llamadas sea más voluminoso en idiomas que no admiten fácilmente las funciones de primera clase.

Usamos CSLA.NET, y se considera que una captura de datos fallida debe devolver un objeto “vacío”. Esto es realmente bastante molesto, ya que exige la convención de comprobar si obj.IsNew rathern than obj == null .

Como se mencionó en un cartel anterior, los valores nulos de retorno provocarán que el código falle de inmediato, reduciendo la probabilidad de problemas ocultos causados ​​por objetos vacíos.

Personalmente, creo que null es más elegante.

Es un caso muy común, y me sorprende que la gente aquí parezca sorprendida: en cualquier aplicación web, los datos a menudo se obtienen usando un parámetro querystring, que obviamente puede ser destruido, por lo que requiere que el desarrollador maneje incidencias de “no encontrado” “.

Podrías manejar esto por:

 if (User.Exists (id)) {
   this.User = User.Fetch (id);
 } else {
   Response.Redirect ("~ / notfound.aspx");
 }

… pero eso es una llamada extra a la base de datos cada vez, lo que puede ser un problema en las páginas con mucho tráfico. Mientras:

 this.User = User.Fetch (id);

 if (this.User == null) {
   Response.Redirect ("~ / notfound.aspx");
 }

… requiere solo una llamada.

Prefiero null , ya que es compatible con el operador nulo-coalescente ( ?? ).

Diría return null en lugar de un objeto vacío.

Pero la instancia específica que ha mencionado aquí, está buscando un usuario por ID de usuario, que es la clave para ese usuario, en ese caso, probablemente desee lanzar una excepción si no se encuentra ninguna instancia de instancia de usuario .

Esta es la regla que generalmente sigo:

  • Si no se encuentra ningún resultado en una operación de búsqueda por clave principal, ejecute ObjectNotFoundException.
  • Si no se encuentra ningún resultado en un hallazgo por ningún otro criterio, devuelva nulo.
  • Si no se encuentra ningún resultado en un hallazgo por un criterio no clave que pueda devolver un objeto múltiple, se devuelve una colección vacía.

Va a variar en función del contexto, pero en general devolveré nulo si estoy buscando un objeto en particular (como en su ejemplo) y devolveré una colección vacía si estoy buscando un conjunto de objetos pero no hay ninguno.

Si cometió un error en su código y devuelve nulos conduce a excepciones de puntero nulo, cuanto antes lo detecte, mejor. Si devuelve un objeto vacío, su uso inicial puede funcionar, pero es posible que obtenga errores más adelante.

Lo mejor en este caso devuelve “nulo” en caso de que no haya un usuario de este tipo. También haga que su método sea estático.

Editar:

Normalmente, métodos como este son miembros de alguna clase de “Usuario” y no tienen acceso a los miembros de su instancia. En este caso, el método debe ser estático; de lo contrario, debe crear una instancia de “Usuario” y luego llamar al método GetUserById, que devolverá otra instancia de “Usuario”. De acuerdo, esto es confuso. Pero si el método GetUserById es miembro de alguna clase de “DatabaseFactory”, no hay problema para dejarlo como un miembro de instancia.

Personalmente devuelvo una instancia predeterminada del objeto. La razón es que espero que el método devuelva cero en muchos o cero en uno (según el propósito del método). La única razón por la que sería un estado de error de cualquier tipo, utilizando este enfoque, es si el método no devolvía ningún objeto (y siempre se esperaba que lo hiciera) (en términos de retorno uno a muchos o singular).

En cuanto a la suposición de que esta es una pregunta de dominio comercial, simplemente no lo veo desde ese lado de la ecuación. La normalización de los tipos de devolución es una pregunta de architecture de aplicación válida. Por lo menos, está sujeto a la estandarización en las prácticas de encoding. Dudo que haya un usuario de negocios que diga “en el escenario X, solo denles un nulo”.

En nuestros Business Objects tenemos 2 métodos principales de obtención:

Para mantener las cosas simples en el contexto o preguntas, serían:

 // Returns null if user does not exist public UserEntity GetUserById(Guid userId) { } // Returns a New User if user does not exist public UserEntity GetNewOrExistingUserById(Guid userId) { } 

El primer método se usa para obtener entidades específicas, el segundo método se usa específicamente al agregar o editar entidades en páginas web.

Esto nos permite tener lo mejor de ambos mundos en el contexto donde se usan.

Soy un estudiante francés de informática, así que disculpe mi pobre inglés. En nuestras clases, se nos dice que dicho método nunca debería devolver un valor nulo, ni un objeto vacío. El usuario de este método debe verificar primero que el objeto que está buscando existe antes de tratar de obtenerlo.

Usando Java, se nos pide que agreguemos un assert exists(object) : "You shouldn't try to access an object that doesn't exist"; al comienzo de cualquier método que pueda devolver nulo, para express la “precondición” (no sé cuál es la palabra en inglés).

IMO esto realmente no es fácil de usar, pero eso es lo que estoy usando, esperando algo mejor.

Si el caso del usuario no se encuentra con la frecuencia suficiente, y desea lidiar con eso de varias maneras dependiendo de las circunstancias (a veces lanzando una excepción, a veces sustituyendo a un usuario vacío) también podría usar algo cercano a la Option F # o Haskell Maybe type, que separa explícitamente el caso ‘sin valor’ de ‘found something!’. El código de acceso a la base de datos podría verse así:

 public Option GetUserById(Guid userId) { //Imagine some code here to access database..... //Check if data was returned and return a null if none found if (!DataExists) return Option.Nothing; else return Option.Just(existingUserEntity); } 

Y ser usado así:

 Option result = GetUserById(...); if (result.IsNothing()) { // deal with it } else { UserEntity value = result.GetValue(); } 

Desafortunadamente, todo el mundo parece lanzar un tipo como este de los suyos.

Normalmente devuelvo nulo. Proporciona un mecanismo rápido y fácil para detectar si algo se arruinó sin tirar excepciones y usar toneladas de try / catch por todas partes.

Para los tipos de recostackción, devolvería una Colección vacía, para todos los demás tipos prefiero usar los patrones NullObject para devolver un objeto que implemente la misma interfaz que el del tipo de devolución. para detalles sobre el patrón, revisa el texto del enlace

Usando el patrón NullObject esto sería:

 public UserEntity GetUserById(Guid userId) 

{// Imagine un código aquí para acceder a la base de datos …..

  //Check if data was returned and return a null if none found if (!DataExists) return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity(); else return existingUserEntity; 

}

 class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 

Para poner lo que otros han dicho de una manera más contundente …

Las excepciones son por circunstancias excepcionales

Si este método es pura capa de acceso a los datos, diría que dado un parámetro que se incluye en una statement de selección, es de esperar que no encuentre ninguna fila desde la cual construir un objeto, y por lo tanto, devolver el valor nulo sería aceptable ya que es la lógica de acceso a datos.

Por otro lado, si esperaba que mi parámetro reflejara una clave principal y solo debería recuperar una fila, si obtuve más de una, lanzaría una excepción. 0 está bien para devolver nulo, 2 no es.

Ahora, si tuviera algún código de inicio de sesión que verificara con un proveedor de LDAP, luego verifiqué con un DB para obtener más detalles y esperé que estuvieran sincronizados en todo momento, entonces podría lanzar la excepción. Como otros dijeron, son reglas comerciales.

Ahora diré que es una regla general . Hay momentos en los que puede querer romper eso. Sin embargo, mi experiencia y experimentos con C # (mucho de eso) y Java (un poco de eso) me han enseñado que es mucho más costoso en cuanto a rendimiento tratar con excepciones que manejar problemas predecibles a través de la lógica condicional. Estoy hablando con una cantidad de 2 o 3 órdenes de magnitud más costosa en algunos casos. Entonces, si es posible que tu código termine en un bucle, entonces te aconsejaría que devuelvas el nulo y lo prueba.

Perdona mi pseudo-php / código.

Creo que realmente depende del uso previsto del resultado.

Si tiene la intención de editar / modificar el valor de retorno y guardarlo, devuelva un objeto vacío. De esta forma, puede usar la misma función para completar datos en un objeto nuevo o existente.

Supongamos que tengo una función que toma una clave principal y una matriz de datos, llena la fila con datos y luego guarda el registro resultante en la base de datos. Como tengo la intención de poblar el objeto con mis datos de cualquier manera, puede ser una gran ventaja recuperar un objeto vacío del captador. De esa forma, puedo realizar operaciones idénticas en cualquier caso. Usas el resultado de la función getter no importa qué.

Ejemplo:

 function saveTheRow($prim_key, $data) { $row = getRowByPrimKey($prim_key); // Populate the data here $row->save(); } 

Aquí podemos ver que la misma serie de operaciones manipula todos los registros de este tipo.

Sin embargo, si la intención final del valor de retorno es leer y hacer algo con los datos, entonces devolvería nulo. De esta forma, puedo determinar rápidamente si no se devolvieron datos y mostrar el mensaje apropiado al usuario.

Por lo general, detectaré excepciones en mi función que recupera los datos (para poder registrar mensajes de error, etc.) y luego devuelvo el nulo directamente desde la captura. En general, no le importa al usuario final cuál es el problema, por lo que me parece mejor encapsular mi registro / procesamiento de errores directamente en la función que obtiene los datos. Si mantiene una base de código compartida en cualquier empresa grande, esto es especialmente beneficioso porque puede forzar el registro / manejo correcto de errores incluso en el progtwigdor más perezoso.

Ejemplo:

 function displayData($row_id) { // Logging of the error would happen in this function $row = getRow($row_id); if($row === null) { // Handle the error here } // Do stuff here with data } function getRow($row_id) { $row = null; try{ if(!$db->connected()) { throw excpetion("Couldn't Connect"); } $result = $db->query($some_query_using_row_id); if(count($result) == 0 ) { throw new exception("Couldn't find a record!"); } $row = $db->nextRow(); } catch (db_exception) { //Log db conn error, alert admin, etc... return null; // This way I know that null means an error occurred } return $row; } 

Esa es mi regla general. Funcionó bien hasta ahora.

Una pregunta interesante y creo que no hay una respuesta “correcta”, ya que siempre depende de la responsabilidad de su código. ¿Su método sabe si ningún dato encontrado es un problema o no? En la mayoría de los casos, la respuesta es “no” y es por eso que devolver el nulo y dejar que la persona que llama manejando la situación sea perfecta.

Tal vez un buen enfoque para distinguir los métodos de arrojar de los métodos de devolución nula es encontrar una convención en su equipo: los métodos que dicen que “obtienen” algo deben arrojar una excepción si no hay nada que obtener. Methods that may return null could be named differently, perhaps “Find…” instead.

If the object returned is something that can be iterated over, I would return an empty object, so that I don’t have to test for null first.

Ejemplo:

 bool IsAdministrator(User user) { var groupsOfUser = GetGroupsOfUser(user); // This foreach would cause a run time exception if groupsOfUser is null. foreach (var groupOfUser in groupsOfUser) { if (groupOfUser.Name == "Administrators") { return true; } } return false; } 

I like not to return null from any method, but to use Option functional type instead. Methods that can return no result return an empty Option, rather than null.

Also, such methods that can return no result should indicate that through their name. I normally put Try or TryGet or TryFind at the beginning of the method’s name to indicate that it may return an empty result (eg TryFindCustomer, TryLoadFile, etc.).

That lets the caller apply different techniques, like collection pipelining (see Martin Fowler’s Collection Pipeline ) on the result.

Here is another example where returning Option instead of null is used to reduce code complexity: How to Reduce Cyclomatic Complexity: Option Functional Type

More meat to grind: let’s say my DAL returns a NULL for GetPersonByID as advised by some. What should my (rather thin) BLL do if it receives a NULL? Pass that NULL on up and let the end consumer worry about it (in this case, an ASP.Net page)? How about having the BLL throw an exception?

The BLL may be being used by ASP.Net and Win App, or another class library – I think it is unfair to expect the end consumer to intrinsically “know” that the method GetPersonByID returns a null (unless null types are used, I guess).

My take (for what it’s worth) is that my DAL returns NULL if nothing is found. FOR SOME OBJECTS, that’s ok – it could be a 0:many list of things, so not having any things is fine (eg a list of favourite books). In this case, my BLL returns an empty list. For most single entity things (eg user, account, invoice) if I don’t have one, then that’s definitely a problem and a throw a costly exception. However, seeing as retrieving a user by a unique identifier that’s been previously given by the application should always return a user, the exception is a “proper” exception, as in it’s exceptional. The end consumer of the BLL (ASP.Net, f’rinstance) only ever expects things to be hunky-dory, so an Unhandled Exception Handler will be used instead of wrapping every single call to GetPersonByID in a try – catch block.

If there is a glaring problem in my approach, please let me know as I am always keen to learn. As other posters have said, exceptions are costly things, and the “checking first” approach is good, but exceptions should be just that – exceptional.

I’m enjoying this post, lot’s of good suggestions for “it depends” scenarios 🙂

I think functions should not return null, for the health of your code-base. I can think of a few reasons:

There will be a large quantity of guard clauses treating null reference if (f() != null) .

What is null , is it an accepted answer or a problem? Is null a valid state for a specific object? (imagine that you are a client for the code). I mean all reference types can be null, but should they?

Having null hanging around will almost always give a few unexpected NullRef exceptions from time to time as your code-base grows.

There are some solutions, tester-doer pattern or implementing the option type from functional programming.

I am perplexed at the number of answers (all over the web) that say you need two methods: an “IsItThere()” method and a “GetItForMe()” method and so this leads to a race condition. What is wrong with a function that returns null, assigning it to a variable, and checking the variable for Null all in one test? My former C code was peppered with

if ( NULL != (variable = function(arguments…)) ) {

So you get the value (or null) in a variable, and the result all at once. Has this idiom been forgotten? ¿Por qué?

I agree with most posts here, which tend towards null .

My reasoning is that generating an empty object with non-nullable properties may cause bugs. For example, an entity with an int ID property would have an initial value of ID = 0 , which is an entirely valid value. Should that object, under some circumstance, get saved to database, it would be a bad thing.

For anything with an iterator I would always use the empty collection. Algo como

 foreach (var eachValue in collection ?? new List(0)) 

is code smell in my opinion. Collection properties shouldn’t be null, ever.

An edge case is String . Many people say, String.IsNullOrEmpty isn’t really necessary, but you cannot always distinguish between an empty string and null. Furthermore, some database systems (Oracle) won’t distinguish between them at all ( '' gets stored as DBNULL ), so you’re forced to handle them equally. The reason for that is, most string values either come from user input or from external systems, while neither textboxes nor most exchange formats have different representations for '' and null . So even if the user wants to remove a value, he cannot do anything more than clearing the input control. Also the distinction of nullable and non-nullable nvarchar database fields is more than questionable, if your DBMS is not oracle – a mandatory field that allows '' is weird, your UI would never allow this, so your constraints do not map. So the answer here, in my opinion is, handle them equally, always.

Concerning your question regarding exceptions and performance: If you throw an exception which you cannot handle completely in your program logic, you have to abort, at some point, whatever your program is doing, and ask the user to redo whatever he just did. In that case, the performance penalty of a catch is really the least of your worries – having to ask the user is the elephant in the room (which means re-rendering the whole UI, or sending some HTML through the internet). So if you don’t follow the anti-pattern of ” Program Flow with Exceptions “, don’t bother, just throw one if it makes sense. Even in borderline cases, such as “Validation Exception”, performance is really not an issue, since you have to ask the user again, in any case.

An Asynchronous TryGet Pattern:

For synchronous methods, I believe @Johann Gerell’s answer is the pattern to use in all cases.

However the TryGet pattern with the out parameter does not work with Async methods.

With C# 7’s Tuple Literals you can now do this:

 async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id) { if (InternalIdExists(id)) { o = await InternalGetSomeObjectAsync(id); return (true, o); } else { return (false, default(SomeObject)); } }