¿Deberían las funciones devolver un objeto nulo o vacío?


209

¿Cuál es la mejor práctica al devolver datos de funciones? ¿Es mejor devolver un objeto nulo o 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 programa y que no habría 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 afectar el rendimiento.


44
Creo que te refieres if (!DataExists).
Sarah Vessels

107
Esta es una pregunta arquitectónica y es perfectamente apropiada. La pregunta del OP es válida independientemente del problema comercial que está tratando de resolver.
Joseph Ferris el

2
Esta pregunta ya ha sido suficientemente respondida. Creo que esta es una pregunta muy interesante.
John Scipione

'getUser ()' debería devolver nulo. 'getCurrentUserInfo ()' o 'getCurrentPermissions ()', OTOH, serían preguntas más reveladoras: deberían devolver un objeto de respuesta no nulo independientemente de quién / o si alguien haya iniciado sesión.
Thomas W

2
No @ Bergi, el otro es un duplicado. Primero se le preguntó a la mía, en octubre, a la otra tres meses después en diciembre. Además, el otro habla de una colección que es ligeramente diferente.
7wp

Respuestas:


207

Por lo general, la mejor idea es devolver nulo 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 nulo indica claramente que no se ha devuelto nada.

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


21
Debería lanzar una excepción, no tragar el problema y volver nulo. Como mínimo, debe registrar esto y luego continuar.
Chris Ballance

130
@ Chris: No estoy de acuerdo. Si el código documenta claramente que el valor de retorno es nulo, es perfectamente aceptable devolverlo si no se encuentra ningún resultado que coincida con sus criterios. Lanzar una excepción debería ser tu ÚLTIMA opción, no tu PRIMERA.
Mike Hofer

12
@ Chris: ¿Sobre qué base decides esto? Agregar registros a la ecuación ciertamente parece excesivo. Deje que el código consumidor decida qué, en todo caso, debe hacerse en caso de que no haya ningún usuario. Al igual que con mi comentario anterior, no hay absolutamente ningún problema al devolver un valor que se define universalmente como "sin datos".
Adam Robinson

17
Estoy un poco desconcertado de que un desarrollador de Microsoft crea que "devolver nulo" equivale a "tragarse el problema". Si la memoria sirve, hay toneladas de métodos en el Marco donde se devuelven los métodos nulos si no hay nada que coincida con la solicitud del llamante. ¿Es eso "tragarse el problema"?
Mike Hofer

55
Por último, pero no menos importante, habría bool GetUserById(Guid userId, out UserEntity result), lo que preferiría al valor de retorno "nulo", y que no es tan extremo como lanzar una excepción. Permite nullcódigo hermoso como libre if(GetUserById(x,u)) { ... }.
Marcel Jackwerth

44

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 asumir con seguridad que si un usuario NO existe, el código de llamada intenta 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 preocupaciones / punto de vista de SRP, los dos primeros son más correctos. Y técnicamente, el primero es el más correcto (pero solo por un pelo): GetUserById solo debe ser responsable de una cosa: obtener al usuario. Manejar su propio caso de "usuario no existe" devolviendo algo más podría ser una violación de SRP. Separarse en un cheque diferente: bool DoesUserExist(id)sería apropiado si elige lanzar una excepción.

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

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


Alguien te rechazó y yo te voté nuevamente, ya que esto no me parece una mala respuesta; excepto: Nunca arrojaría una excepción al no encontrar ningún usuario en un método como el que ofrece el póster. Si encontrar ningún usuario implica una identificación válida o algún problema de este tipo de excepciones dignas, que debería ocurrir más arriba - las necesidades del método de lanzamiento para saber más acerca de dónde provenía de ese ID, etc.
Jacob Mattison

(Supongo que el voto negativo fue una objeción a la idea de lanzar una excepción en una circunstancia como esta.)
Jacob Mattison

1
De acuerdo hasta su último punto. No hay violación de SRP al devolver un valor que se define universalmente como "sin datos". Es como decir que una base de datos SQL debería devolver un error si una cláusula where no produce resultados. Si bien una excepción es una opción de diseño válida (aunque sería una que me irritaría como consumidor), no es "más correcta" que devolver nulo. Y no, no soy el DV.
Adam Robinson

@JacobM lanzamos excepciones cuando exigimos una ruta del sistema de archivos que no existe, no devuelve nulo, pero no de bases de datos. Claramente, ambos son apropiados, que es a lo que me refiero, solo depende.
Rex M

2
@Charles: Estás respondiendo la pregunta "si se lanza una excepción en algún momento", pero la pregunta es "si esta función arroja la excepción". La respuesta correcta es "tal vez", no "sí".
Adam Robinson el

30

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


Estoy a punto de agregar esto como respuesta. NullObjectPattern o patrón de caso especial. Entonces podría implementar uno para cada caso, NoUserEntitiesFound, NullUserEntities, etc.
David Swindells

27

Si su tipo de retorno es una matriz, devuelva una matriz vacía; de lo contrario, devuelva nulo.


¿Es 0 elementos en una lista lo mismo que la lista sin asignar en este momento?
AnthonyWJones

3
0 elementos en una lista no es lo mismo que null. Le permite usarlo en foreachdeclaraciones y consultas linq sin preocuparse NullReferenceException.
Darin Dimitrov

55
Me sorprende que esto no se haya votado más. Esto me parece una guía bastante razonable.
shashi

Bueno, el contenedor vacío es solo una instancia específica del patrón de objeto nulo. Lo que podría ser apropiado, no podemos decirlo.
Deduplicador

Devolver una matriz vacía cuando los datos no están disponibles es simplemente incorrecto . Hay una diferencia entre los datos disponibles y que no contienen elementos y los datos que no están disponibles. Devolver una matriz vacía en ambos casos hace imposible saber cuál es el caso. Hacerlo sólo para que pueda utilizar un foreach sin comprobar si los datos aún existe está más allá de tonto - la persona que llama debe tener para comprobar si existen los datos y una NullReferenceException si la persona que llama no comprueba es buena porque expone un error ..
Reinstale a Monica el

12

Debe lanzar una excepción (solo) si se rompe un contrato específico.
En su ejemplo específico, al solicitar una entidad de usuario basada en un ID conocido, dependería del hecho si los usuarios faltantes (eliminados) son un caso esperado. Si es así, regrese, nullpero si no es un caso esperado, arroje una excepción.
Tenga en cuenta que si se llamó a la función UserEntity GetUserByName(string name), probablemente no arrojaría sino que devolvería nulo. En ambos casos, devolver una UserEntity vacía sería inútil.

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


Me alegra que hayas mencionado que las cadenas son diferentes, ya que Google me mostró esto cuando estaba decidiendo si devolver una cadena vacía.
Noumenon

Las cadenas, colecciones y matrices no son diferentes. Si MS lo dice, MS está equivocado. Hay una diferencia entre una cadena vacía y nula, y entre una colección vacía y nula. En ambos casos, el primero representa los datos existentes (de tamaño 0) y el segundo representa la falta de datos. En algunos casos la distinción es muy importante. Por ejemplo, si busca una entrada en un caché, desea saber la diferencia entre los datos que se almacenan en caché pero están vacíos y los datos que no se almacenan en caché, de modo que debe obtenerlos del origen de datos subyacente, donde podría no estar vacío.
Restablece a Mónica el

1
Parece perder el punto y el contexto. .Wher(p => p.Lastname == "qwerty")debería devolver una colección vacía, no null.
Henk Holterman

@HenkHolterman Si puede acceder a la colección completa y aplicar un filtro que no acepta elementos en la colección, el resultado correcto es una colección vacía. Pero si la colección completa no existe, una colección vacía es extremadamente engañosa: nula o arrojada sería correcta dependiendo de si la situación es normal o excepcional. Como su publicación no calificó de qué situación estaba hablando (y ahora aclara que está hablando de la situación anterior) y como el OP estaba hablando de la última situación, tengo que estar en desacuerdo con usted.
Vuelva a instalar Monica

11

Esta es una pregunta comercial, que depende de si la existencia de un usuario con un Id. De Guid específico es un caso de uso normal esperado para esta función, o es una anomalía que evitará que la aplicación complete con éxito cualquier función que este método esté proporcionando al usuario oponerse a...

Si se trata de una "excepción", ya que la ausencia de un usuario con ese Id evitará que la aplicación complete con éxito cualquier función que esté haciendo (supongamos que estamos creando una factura para un cliente al que hemos enviado el producto ... ), esta situación debería generar una excepción ArgumentException (o alguna otra excepción personalizada).

Si un usuario perdido 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 de negocios, uno o más de los cuales requieren un Usuario para completar con éxito, y uno o más de los cuales pueden completar con éxito sin un usuario, entonces la excepción se debe lanzar más arriba en la pila 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 está lanzando la excepción) solo deben comunicar que no existe ningún usuario (nulo, booleano, lo que sea; este es un detalle de implementación).

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


-1 para el votante, +1 para Charles: es una cuestión de negocios y no hay una mejor práctica para esto.
Austin Salonen

Está cruzando los arroyos. La lógica del negocio determina si es o no una "condición de error". Cómo manejar eso es una decisión de arquitectura de la aplicación. La lógica empresarial no va a dictar que se devuelva un valor nulo, solo que se cumplen los requisitos. Si la empresa decide los tipos de devolución de métodos, entonces están demasiado involucrados en el aspecto técnico de la implementación.
Joseph Ferris el

@joseph, el principio central detrás del "manejo estructurado de excepciones" es que las excepciones deberían ser lanzadas cuando los métodos no pueden completar cualquier función que fueron codificadas para implementar. Tiene razón, en el sentido de que si la función comercial que este método ha sido codificado para implementarse se puede "completar con éxito" (lo que sea que eso signifique en el Modelo de dominio), entonces no necesita lanzar una excepción, podría devolver un valor nulo , o una variable booleana "FoundUser", o lo que sea ... Cómo se comunica al método de llamada que no se encontró ningún usuario se convierte en un detalle de implementación técnica.
Charles Bretana

10

Personalmente, devolvería nulo, porque así es como esperaría que actúe la capa DAL / Repository.

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

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


7

tiendo a

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

Distingo 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;
}

Quieres decir que outnoref
Matt Ellen

@ Matt: Sí, señor, ¡ciertamente lo hago! Fijo.
Johann Gerell

2
¡Realmente, esta es la única respuesta única para todos, y por lo tanto, la única y absoluta verdad! :) Sí, depende de los supuestos en función de los cuales se invoca el método ... Entonces, primero aclare estos supuestos y luego elija la combinación correcta de los anteriores. Tuve que desplazarme demasiado hacia abajo para llegar aquí :) +100
yair

Este parece ser el patrón a utilizar, excepto que no admite métodos asíncronos. Hice referencia a esta respuesta y agregué una solución asíncrona con Tuple Literals -> aquí
ttugates el

6

Otro enfoque implica pasar un objeto de devolución de llamada o delegado que operará en el valor. Si no se encuentra un valor, no se llama la devolución de llamada.

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

Esto funciona bien cuando desea evitar verificaciones nulas en todo su código, y cuando no encuentra un valor no es un error. También puede proporcionar una devolución de llamada para cuando no se encuentran objetos si necesita un 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 verse así:

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

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


Interesante. Cuando comenzaste a hablar de delegados, inmediatamente comencé a preguntarme si las expresiones Lambda podrían usarse aquí.
7wp

¡Sip! Según tengo entendido, la sintaxis lambda de C # 3.0 y superior es básicamente azúcar sintáctica para delegados anónimos. Del mismo modo, en Java, sin la agradable sintaxis lambda o delegado anónimo, simplemente puede crear una clase anónima. Es un poco más feo, pero puede ser muy útil. Supongo que en estos días, mi ejemplo de C # podría haber usado Func <UserEntity> o algo así en lugar de un delegado nombrado, pero el último proyecto de C # en el que estaba todavía usaba la versión 2.
Marc

+1 Me gusta este enfoque. Sin embargo, un problema es que no es convencional y aumenta ligeramente la barrera de entrada para una base de código.
timoxley

4

Usamos CSLA.NET, y considera que una recuperación de datos fallida debería devolver un objeto "vacío". En realidad, esto es bastante molesto, ya que exige la convención de verificar si obj.IsNewes más que no obj == null.

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

Personalmente, creo que nulles más elegante.

Es un caso muy común, y me sorprende que la gente aquí parezca sorprendida por él: en cualquier aplicación web, los datos a menudo se obtienen utilizando un parámetro de cadena de consulta, que obviamente puede ser maltratado, por lo que requiere que el desarrollador maneje las incidencias de "no encontrado ".

Podrías manejar esto de la siguiente manera:

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

... pero esa es una llamada adicional a la base de datos cada vez, lo que puede ser un problema en las páginas de alto tráfico. Mientras:

this.User = User.Fetch (id);

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

... requiere solo una llamada.


4

Prefiero null, ya que es compatible con el operador de fusión nula ( ??).


4

Yo diría que devuelva nulo en lugar de un objeto vacío.

Pero en la instancia específica que ha mencionado aquí, está buscando un usuario por ID de usuario, que es una especie de clave para ese usuario, en ese caso probablemente me gustaría lanzar una excepción si no se encuentra una 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 primaria, arroje 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 una búsqueda por un criterio no clave que puede devolver varios objetos, devolverá una colección vacía.

¿Por qué lanzarías una excepción en alguno de estos casos? A veces los usuarios no existen en la base de datos, y esperamos que eso no suceda. No es un comportamiento excepcional.
siride

3

Varía según el contexto, pero generalmente devolveré nulo si busco un objeto en particular (como en su ejemplo) y devolveré una colección vacía si busco un conjunto de objetos pero no hay ninguno.

Si ha cometido un error en su código y devuelve nulo conduce a excepciones de puntero nulo, entonces cuanto antes lo detecte, mejor. Si devuelve un objeto vacío, su uso inicial puede funcionar, pero puede obtener errores más adelante.


+1 Estaba cuestionando la misma lógica que estás diciendo aquí, por eso publiqué la pregunta para ver cuáles serían las opiniones de otros sobre esto
7wp

3

Lo mejor en este caso devuelve "nulo" en el caso de que no exista dicho usuario. También haga que su método sea estático.

Editar:

Por lo general, 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 "DatabaseFactory", no hay problema para dejarlo como miembro de instancia.


¿Puedo preguntar por qué querría que mi método fuera estático? ¿Qué sucede si quiero usar la inyección de dependencia?
7wp

Ok, ahora entiendo tu lógica. Pero sigo con el patrón Repository, y me gusta usar la inyección de dependencia para mis repositorios, por lo tanto, no puedo usar métodos estáticos. Pero +1 por sugerir devolver nulo :)
7wp

3

Yo personalmente devuelvo una instancia predeterminada del objeto. La razón es que espero que el método devuelva cero a muchos o cero a uno (dependiendo del 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 devolvió ningún objeto (s) y siempre se esperaba que (en términos de un retorno de uno a muchos o singular).

En cuanto a la suposición de que esta es una pregunta de dominio comercial, simplemente no la veo desde ese lado de la ecuación. La normalización de los tipos de retorno es una pregunta válida de arquitectura de la aplicación. Como mínimo, está sujeto a estandarización en las prácticas de codificación. Dudo que haya un usuario comercial que diga "en el escenario X, simplemente déle un valor nulo".


+1 Me gusta la vista alternativa del problema. Entonces, ¿básicamente estás diciendo que cualquier enfoque que elija debería estar bien siempre que el método sea consistente en toda la aplicación?
7wp

1
Esa es mi creencia. Creo que la consistencia es extremadamente importante. Si hace las cosas de múltiples maneras en varios lugares, presenta un mayor riesgo de nuevos errores. Personalmente, hemos seguido el enfoque de objeto predeterminado, porque funciona bien con el patrón de Esencia que usamos en nuestro modelo de dominio. Tenemos un único método de extensión genérico que podemos probar contra todos los objetos de dominio para decirnos si está poblado o no, para que sepamos que cualquier DO se puede probar con una llamada de objectname. IsDefault () - evitando cualquier verificación de igualdad directamente .
Joseph Ferris el

3

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

Para mantener las cosas simples en el contexto o si cuestionas, 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 al 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.


3

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

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

En mi opinión, esto no es realmente fácil de usar, pero eso es lo que estoy usando, esperando algo mejor.


1
Gracias por tu respuesta. Pero no me gusta la idea de comprobar primero si existe. La razón es que genera una consulta adicional a la base de datos. En una aplicación a la que acceden millones de personas en un día, esto puede resultar en una pérdida dramática de rendimiento.
7wp

1
Un beneficio es que la verificación de existencia es apropiadamente abstracta: si (userExists) es ligeramente más legible, más cercano al dominio del problema y menos 'computarizado' que: if (user == null)
timoxley

Y argumentaría que 'if (x == null)' es un patrón de décadas de antigüedad que si no lo ha visto antes, no ha escrito código durante mucho tiempo (y debería acostumbrarse a él tal como está en millones de líneas de código). "Computación"? Estamos hablando del acceso a la base de datos ...
Lloyd Sargent

3

Si el caso de que el usuario no ser descubierto se acerca bastante a menudo, y desea que lidiar con eso de varias maneras dependiendo de las circunstancias (a veces lanzando una excepción, a veces la sustitución de un usuario vacío) también se podría utilizar algo cercano a F # 's Optiono Haskell de Maybetipo , que separa explícitamente el caso 'sin valor' de '¡encontró algo!'. El código de acceso a la base de datos podría verse así:

public Option<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 Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

Y se usa así:

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

Desafortunadamente, todo el mundo parece tener un tipo como este.


2

Normalmente vuelvo nulo. Proporciona un mecanismo rápido y fácil para detectar si algo se estropeó sin lanzar excepciones y usar toneladas de try / catch en todo el lugar.


2

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

Usando el patrón NullObject esto sería: -

public UserEntity GetUserById(Guid userId)

{// Imagina 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 ""; } ...} 

2

Para decir lo que otros han dicho de una manera más concisa ...

Las excepciones son por circunstancias excepcionales

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

Por otro lado, si esperaba que mi parámetro reflejara una clave primaria y solo recuperase una fila, si obtuviera más de una, arrojaría una excepción. 0 está bien para devolver nulo, 2 no lo está.

Ahora, si tuviera algún código de inicio de sesión que verificara con un proveedor LDAP y luego con una base de datos para obtener más detalles y esperaba que deberían estar sincronizados en todo momento, entonces podría lanzar la excepción. Como otros dijeron, son las reglas de negocios.

Ahora diré que es una regla general . Hay momentos en los que es posible que desee 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 manejar las excepciones que manejar problemas predecibles a través de la lógica condicional. Estoy hablando de la melodía de 2 o 3 órdenes de magnitud más cara en algunos casos. Entonces, si es posible, su código podría terminar en un bucle, entonces recomendaría devolver nulo y probarlo.


2

Perdona mi pseudo-php / code.

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 esa manera, puede usar la misma función para llenar datos en un objeto nuevo o existente.

Digamos que tengo una función que toma una clave primaria 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 llenar el objeto con mis datos de cualquier manera, puede ser una gran ventaja recuperar un objeto vacío del getter. De esa manera, puedo realizar operaciones idénticas en cualquier caso. Utiliza el resultado de la función getter sin importar 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 manera, 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 nulo directamente desde la captura. Por lo general, no le importa al usuario final cuál es el problema, por lo que creo que es 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 de errores adecuado incluso en el programador más vago.

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. Ha funcionado bien hasta ahora.


2

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 los datos encontrados no son un problema o no? En la mayoría de los casos, la respuesta es "no" y es por eso que volver nulo y dejar que la persona que llama maneje su situación es perfecta.

Quizás un buen enfoque para distinguir los métodos de lanzamiento de los métodos de retorno nulo es encontrar una convención en su equipo: los métodos que dicen que "obtienen" algo deberían generar una excepción si no hay nada que obtener. Los métodos que pueden devolver nulo podrían denominarse de manera diferente, quizás "Buscar ..." en su lugar.


+1 Me gusta la idea de utilizar una convención de nomenclatura uniforme para indicar al programador cómo se va a consumir esa función.
7wp

1
De repente, reconozco que esto es lo que hace LINQ: considerar Primero (...) vs. PrimeroOrDefault (...)
Marc Wittke

2

Si el objeto devuelto es algo que se puede iterar, devolvería un objeto vacío, para que no tenga que probar nulo primero.

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;
}

2

Me gusta no devolver nulo de ningún método, sino usar el tipo funcional Opción en su lugar. Los métodos que no pueden devolver ningún resultado devuelven una Opción vacía, en lugar de nula.

Además, los métodos que no pueden devolver ningún resultado deben indicarlo a través de su nombre. Normalmente pongo Try o TryGet o TryFind al comienzo del nombre del método para indicar que puede devolver un resultado vacío (por ejemplo, TryFindCustomer, TryLoadFile, etc.).

Eso le permite a la persona que llama aplicar diferentes técnicas, como la canalización de la colección (consulte la tubería de la colección de Martin Fowler ) en el resultado.

Aquí hay otro ejemplo en el que se usa la opción Devolución en lugar de nula para reducir la complejidad del código: Cómo reducir la complejidad ciclomática: Tipo funcional de la opción


1
Escribí una respuesta, puedo ver que es similar a la suya a medida que avanzaba, y estoy de acuerdo, puede implementar un tipo de opción con una colección genérica con 0 o 1 elementos. Gracias por los enlaces adicionales.
Gabriel P.

1

Más carne para moler: digamos que mi DAL devuelve un NULL para GetPersonByID según lo aconsejado por algunos. ¿Qué debe hacer mi BLL (más bien delgado) si recibe un NULL? ¿Pasar ese NULL y dejar que el consumidor final se preocupe por ello (en este caso, una página ASP.Net)? ¿Qué tal si el BLL lanza una excepción?

ASP.Net y Win App, u otra biblioteca de clase, pueden estar utilizando BLL. Creo que es injusto esperar que el consumidor final "sepa" intrínsecamente que el método GetPersonByID devuelve un valor nulo (a menos que se utilicen tipos nulos, supongo )

Mi opinión (por lo que vale) es que mi DAL devuelve NULL si no se encuentra nada. PARA ALGUNOS OBJETOS, está bien, podría ser un 0: muchas listas de cosas, por lo que no tener nada está bien (por ejemplo, una lista de libros favoritos). En este caso, mi BLL devuelve una lista vacía. Para la mayoría de las cosas de una sola entidad (por ejemplo, usuario, cuenta, factura) si no tengo una, entonces definitivamente es un problema y una excepción costosa. Sin embargo, dado que la recuperación de un usuario mediante un identificador único que la aplicación ha proporcionado previamente siempre debe devolver un usuario, la excepción es una excepción "adecuada", ya que es excepcional. El consumidor final de BLL (ASP.Net, f'rinstance) solo espera que las cosas sean muy buenas, por lo que se usará un controlador de excepciones no manejadas en lugar de ajustar cada llamada a GetPersonByID en un bloque try-catch.

Si hay un problema evidente en mi enfoque, avíseme ya que siempre estoy ansioso por aprender. Como han dicho otros carteles, las excepciones son costosas, y el enfoque de "comprobar primero" es bueno, pero las excepciones deberían ser solo eso: excepcional.

Estoy disfrutando de esta publicación, muchas buenas sugerencias para escenarios de "depende" :-)


Y, por supuesto, hoy me he encontrado con un escenario en el que voy a devolver NULL de mi BLL ;-) Dicho esto, todavía podría lanzar una excepción y usar try / catch en mi clase de consumo, pero todavía tengo un problema : ¿cómo sabe mi clase consumidora usar un try / catch, similar a cómo saben verificar NULL?
Mike Kingscott el

Puede documentar que un método arroja una excepción a través de @throws doctag y documentaría el hecho de que puede volver nulo en @return doctag.
timoxley

1

Creo que las funciones no deberían devolver nulo, por la salud de su base de código. Se me ocurren algunas razones:

Habrá una gran cantidad de cláusulas de protección que tratan la referencia nula if (f() != null) .

Que es null , es una respuesta aceptada o un problema? ¿Es nulo un estado válido para un objeto específico? (imagina que eres un cliente para el código). Quiero decir que todos los tipos de referencia pueden ser nulos, pero ¿deberían?

Tener que nullpasar el rato casi siempre dará algunas excepciones inesperadas de NullRef de vez en cuando a medida que crece su base de código.

Hay algunas soluciones, tester-doer patterno la implementación de la option typeprogramación funcional.


0

Estoy perplejo por la cantidad de respuestas (en toda la web) que dicen que necesita dos métodos: un método "IsItThere ()" y un método "GetItForMe ()" y esto conduce a una condición de carrera. ¿Qué tiene de malo una función que devuelve un valor nulo, que se asigna a una variable y que verifica la variable para obtener un valor nulo en una sola prueba? Mi antiguo código C estaba salpicado de

if (NULL! = (variable = función (argumentos ...))) {

Entonces obtienes el valor (o nulo) en una variable y el resultado de una vez. ¿Se ha olvidado este idioma? ¿Por qué?


0

Estoy de acuerdo con la mayoría de las publicaciones aquí, que tienden hacia null .

Mi razonamiento es que generar un objeto vacío con propiedades no anulables puede causar errores. Por ejemplo, una entidad con una int IDpropiedad tendría un valor inicial deID = 0 , que es un valor completamente válido. Si ese objeto, bajo alguna circunstancia, se guarda en la base de datos, sería algo malo.

Para cualquier cosa con un iterador, siempre usaría la colección vacía. Algo como

foreach (var eachValue in collection ?? new List<Type>(0))

es código olor en mi opinión. Las propiedades de la colección no deben ser nulas, nunca.

Un caso de borde es String. Mucha gente dice String.IsNullOrEmptyque no es realmente necesario, pero no siempre se puede distinguir entre una cadena vacía y una nula. Además, algunos sistemas de bases de datos (Oracle) no distinguen entre ellos ( ''se almacenan como DBNULL), por lo que se ve obligado a manejarlos por igual. La razón de esto es que la mayoría de los valores de cadena provienen de la entrada del usuario o de sistemas externos, mientras que ni los cuadros de texto ni la mayoría de los formatos de intercambio tienen representaciones diferentes para ''y null. Entonces, incluso si el usuario desea eliminar un valor, no puede hacer nada más que borrar el control de entrada. También la distinción de anulable y no anulablenvarchar campos de bases de datos es más que cuestionable, si su DBMS no es oráculo, un campo obligatorio que permite''es extraño, su IU nunca lo permitiría, por lo que sus restricciones no se asignan. Entonces la respuesta aquí, en mi opinión es, manejarlos por igual, siempre.

Con respecto a su pregunta con respecto a las excepciones y el rendimiento: si lanza una excepción que no puede manejar completamente en la lógica de su programa, debe cancelar, en algún momento, lo que sea que esté haciendo su programa, y ​​pedirle al usuario que vuelva a hacer lo que acaba de hacer. En ese caso, la penalización de rendimiento de a catches realmente la menor de sus preocupaciones: tener que preguntar al usuario es el elefante en la sala (lo que significa volver a representar toda la interfaz de usuario o enviar algo de HTML a través de Internet). Por lo tanto, si no sigue el antipatrón de " Flujo de programa con excepciones ", no se moleste, simplemente tire uno si tiene sentido. Incluso en casos límite, como "Excepción de validación", el rendimiento realmente no es un problema, ya que debe preguntar al usuario nuevamente, en cualquier caso.


0

Un patrón asincrónico TryGet:

Para los métodos sincrónicos, creo que la respuesta de @Johann Gerell es el patrón a utilizar en todos los casos.

Sin embargo, el patrón TryGet con el outparámetro no funciona con los métodos asíncronos.

Con Tuple Literals de C # 7 ahora puedes hacer esto:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.