¿Se debe verificar si el valor nulo no es nulo?

106

La semana pasada, tuvimos un acalorado argumento sobre el manejo de nulos en la capa de servicio de nuestra aplicación. La pregunta está en el contexto de .NET, pero será la misma en Java y en muchas otras tecnologías.

La pregunta era: ¿debería verificar siempre los nulos y hacer que su código funcione sin importar qué, o dejar que se produzca una excepción cuando se recibe un nulo inesperadamente?

Por un lado, comprobar si hay un valor nulo en el que no lo esperas (es decir, no tengo una interfaz de usuario para manejarlo) es, en mi opinión, lo mismo que escribir un bloque try con un retén vacío. Estás ocultando un error. El error podría ser que algo haya cambiado en el código y que el valor nulo sea ahora un valor esperado, o que haya algún otro error y que se haya pasado una identificación incorrecta al método.

Por otro lado, la comprobación de nulos puede ser un buen hábito en general. Además, si hay una verificación, la aplicación puede seguir funcionando, con solo una pequeña parte de la funcionalidad que no tiene ningún efecto. Entonces, el cliente puede informar un error pequeño como "no se puede eliminar el comentario" en lugar de un error mucho más grave como "no se puede abrir la página X".

¿Qué práctica sigues y cuáles son tus argumentos a favor o en contra de cualquiera de los dos enfoques?

Actualización:

Quiero agregar algunos detalles sobre nuestro caso particular. Recuperamos algunos objetos de la base de datos y los procesamos (digamos, construimos una colección). El desarrollador que escribió el código no anticipó que el objeto pudiera ser nulo, por lo que no incluyó ninguna comprobación, y cuando se cargó la página se produjo un error y no se cargó toda la página.

Obviamente, en este caso debería haber habido una verificación. Luego tuvimos una discusión sobre si cada objeto que se procesa debería verificarse, incluso si no se espera que falte, y si el procesamiento final debe abortarse en silencio.

El beneficio hipotético sería que la página continuará funcionando. Piense en los resultados de una búsqueda en Stack Exchange en diferentes grupos (usuarios, comentarios, preguntas). El método puede comprobar si hay un valor nulo y abortar el procesamiento de los usuarios (que debido a un error es nulo), pero devuelve las secciones "comentarios" y "preguntas". La página continuaría trabajando, excepto que faltará la sección "usuarios" (que es un error). ¿Deberíamos fallar antes y romper la página completa o continuar trabajando y esperar a que alguien note que falta la sección "usuarios"?

    
pregunta Stilgar 06.05.2012 - 17:42

16 respuestas

97

La pregunta no es tanto si debe verificar null o dejar que el tiempo de ejecución lance una excepción; es cómo debe responder a una situación tan inesperada.

Sus opciones, entonces, son:

  • Lance una excepción genérica ( NullReferenceException ) y deje que crezca; Si no realiza la comprobación null , esto es lo que sucede automáticamente.
  • Lanzar una excepción personalizada que describa el problema en un nivel superior; esto se puede lograr ya sea lanzando el cheque null , o capturando un NullReferenceException y lanzando la excepción más específica.
  • Recupere sustituyendo un valor predeterminado adecuado.

No hay una regla general sobre cuál es la mejor solución. Personalmente, yo diría:

  • La excepción genérica es mejor si la condición de error sería un signo de un error grave en su código, es decir, el valor que es nulo nunca debe permitirse llegar allí en primer lugar. Una excepción de este tipo puede aparecer en el registro de errores que haya configurado, para que alguien reciba una notificación y corrija el error.
  • La solución de valor predeterminado es buena si proporcionar resultados semi-útiles es más importante que la corrección, como un navegador web que acepta HTML técnicamente incorrecto y hace un mejor esfuerzo para representarlo de manera sensata.
  • De lo contrario, aceptaría la excepción específica y la manejaría en algún lugar adecuado.
respondido por el tdammers 06.05.2012 - 20:58
56

En mi humilde opinión, tratar de manejar valores nulos que no esperas conduce a un código demasiado complicado. Si no espera nulo, hágalo claro lanzando ArgumentNullException . Me siento realmente frustrado cuando la gente comprueba si el valor es nulo y luego intento escribir un código que no tiene ningún sentido. Lo mismo se aplica al uso de SingleOrDefault (o, lo que es peor, obtener la recopilación) cuando alguien realmente espera a Single y muchos otros casos cuando la gente tiene miedo (no sé realmente a qué) a expresar su lógica con claridad.

    
respondido por el empi 06.05.2012 - 17:58
34

El hábito de verificar null según mi experiencia proviene de los antiguos desarrolladores de C o C ++, en esos idiomas es muy probable que ocultes un error grave cuando no comprueba NULL . Como sabes, en Java o C # las cosas son diferentes, usar una referencia nula sin verificar null causará una excepción, por lo tanto, el error no se ocultará en secreto siempre y cuando no lo ignores en el lado de la llamada. Entonces, en la mayoría de los casos, la comprobación explícita de null no tiene mucho sentido y complica el código más de lo necesario. Es por eso que no considero que la verificación nula es un buen hábito en esos idiomas (al menos, no en general).

Por supuesto, hay excepciones a esa regla, aquí están las que se me ocurren:

  • desea un mensaje de error mejor, legible para el ser humano, que indique al usuario en qué parte del programa se produjo la referencia nula incorrecta. Por lo tanto, su prueba para nulos arroja una excepción diferente, con un texto de error diferente. Obviamente, ningún error se ocultará de esa manera.

  • desea que su programa "falle más temprano". Por ejemplo, desea verificar un valor nulo de un parámetro de constructor, donde la referencia del objeto de lo contrario solo se almacenaría en una variable miembro y se usaría más adelante.

  • puede lidiar con la situación de una referencia nula de forma segura, sin el riesgo de fallas posteriores y sin enmascarar un error grave.

  • quiere un tipo de señalización de error completamente diferente (por ejemplo, devolver un código de error) en su contexto actual

respondido por el Doc Brown 06.05.2012 - 18:12
15

Use asevera para probar e indicar condiciones previas / posteriores e invariantes para su código.

Hace que sea mucho más fácil entender qué espera el código y qué se puede esperar que maneje.

Una aserción, IMO, es una verificación adecuada porque es:

  • eficiente (generalmente no se usa en la versión),
  • breve (generalmente una sola línea) y
  • claro (condición previa violada, esto es un error de programación).

Creo que el código de auto-documentación es importante, por lo tanto tener un cheque es bueno. La programación defensiva, a prueba de fallas, facilita el mantenimiento, que es donde usualmente se gasta más tiempo.

Actualizar

Escrito tu elaboracion. Por lo general, es bueno proporcionarle al usuario final una aplicación que funcione bien bajo errores, es decir, que muestre la mayor cantidad posible. La robustez es buena, porque solo el cielo sabe lo que se romperá en un momento crítico.

Sin embargo, los desarrolladores & los evaluadores deben estar al tanto de los errores lo antes posible, por lo que probablemente desee un marco de registro con un gancho que pueda mostrar una alerta al usuario de que hubo un problema. La alerta probablemente debería mostrarse a todos los usuarios, pero probablemente se puede adaptar de manera diferente según el entorno de ejecución.

    
respondido por el Macke 06.05.2012 - 21:51
7

Falla temprano, falla a menudo. null es uno de los mejores valores inesperados que puede obtener, porque puede fallar rápidamente tan pronto como trate de usarlo. Otros valores inesperados no son tan fáciles de detectar. Como null falla automáticamente cuando intentas usarlo, diría que no requiere una comprobación explícita.

    
respondido por el DeadMG 06.05.2012 - 17:57
6
  • La verificación de un nulo debe ser su segunda naturaleza

  • Tu API será utilizada por otras personas y es probable que su expectativa sea diferente a la tuya

  • Las pruebas unitarias completas normalmente resaltan la necesidad de una verificación de referencia nula

  • La comprobación de nulos no implica sentencias condicionales. Si le preocupa que la verificación nula haga que su código sea menos legible, entonces puede considerar los contratos de código .NET.

  • Considere instalar ReSharper (o similar) para buscar en su código las comprobaciones de referencia nulas faltantes

respondido por el CodeART 06.05.2012 - 21:47
4

Consideraría la pregunta desde el punto de vista de la semántica, es decir, preguntarme a mí mismo qué representa un puntero NULO o un argumento de referencia nula.

Si una función o método foo () tiene un argumento x de tipo T, x puede ser obligatorio o opcional .

En C ++ puede pasar un argumento obligatorio como

  • Un valor, por ejemplo, void foo (T x)
  • Una referencia, por ejemplo. void foo (T & x).

En ambos casos, no tiene el problema de comprobar un puntero NULO. Por lo tanto, en muchos casos, no necesita una marca de puntero nulo en absoluto para los argumentos obligatorios.

Si está pasando un argumento opcional , puede usar un puntero y usar el valor NULO para indicar que no se dio ningún valor:

void foo(T* x)

Una alternativa es usar un puntero inteligente como

void foo(shared_ptr<T> x)

En este caso, es probable que desee comprobar el puntero en el código, ya que hace una diferencia para el método foo () si x contiene un valor o ningún valor.

El tercer y último caso es que utiliza un puntero para un obligatorio argumento. En este caso, llamar a foo (x) con x == NULL es un error y usted tiene que decidir cómo manejar esto (lo mismo que con un índice fuera de límites o cualquier entrada no válida):

  1. Sin verificación: si hay un error, deje que se bloquee y espero que este error aparezca lo suficientemente temprano (durante la prueba). Si no lo hace (si no falla lo suficientemente pronto), espere que no aparezca en un sistema de producción porque la experiencia del usuario será que vea que la aplicación falla.
  2. Verifique todos los argumentos al comienzo del método e informe los argumentos NULL no válidos, por ejemplo, lanza una excepción desde foo () y deja que algún otro método lo maneje. Probablemente, lo mejor que puede hacer es mostrar al usuario una ventana de diálogo que indique que la aplicación ha encontrado un error interno y que se cerrará.

Además de una mejor manera de fallar (brindándole al usuario más información), una ventaja del segundo enfoque es que es más probable que se encuentre el error: el programa fallará cada vez que se llame al método con un error. argumentos, mientras que con el enfoque 1, el error solo aparecerá si se ejecuta una instrucción para eliminar la referencia al puntero (por ejemplo, si se ha introducido la rama derecha de una instrucción if). Así que el enfoque 2 ofrece una forma más fuerte de "fallar antes".

En Java tienes menos opciones porque todos los objetos se pasan usando referencias que siempre pueden ser nulas. Nuevamente, si el valor nulo representa un valor NINGUNO de un argumento opcional, entonces el control del valor nulo será (probablemente) parte de la lógica de implementación.

Si el valor nulo es una entrada no válida, tiene un error y aplicaría consideraciones similares a las del caso de los punteros de C ++. Ya que en Java puede capturar excepciones de puntero nulo, el enfoque 1 anterior es equivalente al enfoque 2 si el método foo () elimina las referencias de todos los argumentos de entrada en todas las rutas de ejecución (lo que probablemente ocurra muy a menudo para métodos pequeños).

Summarizing

  • Tanto en C ++ como en Java, comprobar si los argumentos opcionales son nulos es parte de la lógica del programa.
  • En C ++, siempre verifico argumentos obligatorios para garantizar que se genere una excepción adecuada para que el programa pueda manejarlo de manera sólida.
  • En Java (o C #), marcaría argumentos obligatorios si hay rutas de ejecución que no se lanzan para tener una forma más fuerte de "fallar antes".

EDIT

Gracias a Stilgar por más detalles.

En su caso, parece que tiene un valor nulo como resultado de un método. De nuevo, creo que primero debe aclarar (y corregir) la semántica de sus métodos antes Puedes tomar una decisión.

Entonces, tienes el método m1 () llamando al método m2 () y m2 () devuelve una referencia (en Java) o un puntero (en C ++) a algún objeto.

¿Cuál es la semántica de m2 ()? ¿Debe m2 () siempre devolver un resultado no nulo? Si este es el caso, entonces un resultado nulo es un error interno (en m2 ()). Si verifica el valor de retorno en m1 () puede tener un manejo de errores más robusto. Si no lo hace, probablemente tendrá una excepción, tarde o temprano. Tal vez no. Espero que la excepción se produzca durante las pruebas y no después de la implementación. Pregunta : si m2 () nunca debe devolver un valor nulo, ¿por qué devuelve un valor nulo? Tal vez debería lanzar una excepción en su lugar? m2 () es probablemente con errores.

La alternativa es que devolver el valor nulo es parte de la semántica del método m2 (), es decir, tiene un resultado opcional, que debe documentarse en la documentación de Javadoc del método. En este caso, está bien tener un valor nulo. El método m1 () debe verificarlo como cualquier otro valor: aquí no hay ningún error, pero probablemente el hecho de verificar si el resultado es nulo es solo parte de la lógica del programa.

    
respondido por el Giorgio 06.05.2012 - 19:51
3

Mi enfoque general es nunca probar una condición de error que no sepa cómo manejar . Luego, la pregunta que debe responderse en cada instancia específica se convierte en: ¿puede hacer algo razonable en presencia del valor inesperado de null ?

Si puede continuar de forma segura sustituyendo otro valor (una colección vacía, digamos, o un valor o instancia por defecto), entonces, por todos los medios, hágalo. el ejemplo de @ Shahbaz de World of Warcraft de sustituir una imagen por otra cae en esa categoría; la imagen en sí es probable que sea solo decoración, y no tiene impacto funcional . (En una versión de depuración, usaría un color que grita para llamar la atención sobre el hecho de que se ha producido una sustitución, pero eso depende en gran medida de la naturaleza de la aplicación). Sin embargo, registre los detalles sobre el error, para que sepa que está ocurriendo; de lo contrario, estará ocultando un error que probablemente desee conocer. Ocultarlo al usuario es una cosa (de nuevo, suponiendo que el procesamiento puede continuar de manera segura); ocultarlo del desarrollador es otra muy distinta.

Si no puede continuar de forma segura en presencia de un valor nulo, entonces falla con un error sensible; en general, prefiero fallar lo antes posible cuando es obvio que la operación no puede tener éxito, lo que implica verificar las condiciones previas. Hay ocasiones en las que no quiere fallar de inmediato, pero aplace la falla por el mayor tiempo posible; esta consideración surge, por ejemplo, en las aplicaciones relacionadas con la criptografía, donde los ataques de canal lateral podrían divulgar información que no desea que se conozca. Al final, deje que el error se convierta en un controlador de errores general, que a su vez registra los detalles relevantes y muestra un mensaje de error agradable (aunque sea agradable) para el usuario.

También vale la pena señalar que la respuesta adecuada puede diferir mucho entre las compilaciones de prueba / control de calidad / depuración y las compilaciones de producción / lanzamiento. En las compilaciones de depuración, me gustaría fallar con anticipación y con mensajes de error muy detallados, para dejar completamente claro que existe un problema y permitir que una autopsia determine qué se necesita hacer para solucionarlo. Las compilaciones de producción, cuando se enfrentan a errores recuperables con seguridad , probablemente deberían favorecer la recuperación.

    
respondido por el a CVn 07.05.2012 - 13:31
2

Claro que NULL no es esperado , ¡pero siempre hay errores!

Creo que debería verificar NULL si no lo espera o no. Déjame darte ejemplos (aunque no están en Java).

  • En World of Warcraft, debido a un error, la imagen de uno de los desarrolladores apareció en un cubo para muchos objetos. Imagínese si el motor de gráficos no esperara NULL pointers, se habría producido un bloqueo, mientras que se convirtió en una experiencia no tan fatal (incluso divertida) para el usuario. Lo mínimo es que no habría perdido inesperadamente su conexión o cualquiera de sus puntos de guardado.

    Este es un ejemplo en el que la comprobación de NULL impidió un bloqueo y el caso se manejó en silencio.

  • Imagina un programa de cajero bancario. La interfaz realiza algunas verificaciones y envía datos de entrada a una parte subyacente para realizar las transferencias de dinero. Si la parte central espera no recibe NULL , entonces un error en la interfaz puede causar un problema en la transacción, posiblemente un poco de dinero en el medio se pierda (o peor, se duplique).

    Por "un problema" me refiero a un bloqueo (como en crash (es decir, el programa está escrito en C ++), no una excepción de puntero nulo). En este caso, la transacción debe revertirse si se encuentra NULL (lo que, por supuesto, no sería posible si no verificara las variables con NULL)

Estoy seguro de que entiendes la idea.

La verificación de la validez de los argumentos de sus funciones no desordena su código, ya que es un par de comprobaciones en la parte superior de su función (no se extiende en el medio) y aumenta considerablemente la solidez.

Si tiene que elegir entre "el programa le dice al usuario que ha fallado y se apaga o retrocede a un estado coherente posiblemente creando un registro de errores" y "Violación de acceso", ¿no elegiría el primero? ? Eso significa que cada función debe estar preparada para ser llamada por un código de error en lugar de esperando todo es correcto, simplemente porque los errores siempre existen.

    
respondido por el Shahbaz 07.05.2012 - 02:00
2

Como señalaron las otras respuestas Si no espera NULL , hágalo claro lanzando ArgumentNullException . En mi opinión, cuando estás depurando el proyecto te ayuda a descubrir las fallas en la lógica de tu programa antes.

Así que va a lanzar su software, si restaura esos NullRefrences cheques, no se pierde nada en la lógica de su software, simplemente se asegura que un error grave no se abrirá.

Otro punto es que si la comprobación de NULL está en los parámetros de una función, entonces puede decidir si la función es el lugar correcto para hacerlo o no; si no, busque todas las referencias a la función y luego, antes de pasar un parámetro a la función, revise los escenarios probables que pueden llevar a una referencia nula.

    
respondido por el Ahmad 02.02.2015 - 19:40
1

Cada null en su sistema es una bomba de tiempo en espera de ser descubierta. Compruebe null en los límites donde su código interactúa con el código que no controla, y prohíba null dentro de su propio código. Esto evita el problema sin confiar ingenuamente en el código extranjero. Utilice excepciones o algún tipo de tipo Maybe / Nothing en su lugar.

    
respondido por el Doval 29.01.2014 - 21:33
0

Mientras que finalmente se lanzará una excepción de puntero nulo, a menudo se lanzará lejos del punto en que se obtuvo el puntero nulo. Hágase un favor y acorte el tiempo necesario para solucionar el error al menos al registrar el hecho de que obtiene un puntero nulo lo antes posible.

Incluso si no sabe qué hacer ahora, registrar el evento le ahorrará tiempo más tarde. Y tal vez el tipo que reciba el boleto podrá usar el tiempo ahorrado para averiguar la respuesta correcta en ese momento.

    
respondido por el Jon Strayer 07.05.2012 - 15:15
-1

Dado que Fail early, fail fast como lo indica @DeadMG (@empi) no es pssible porque la aplicación web debe funcionar bien en bugs deberías hacer cumplir la regla

captura de excepciones sin registro

para que, como desarrolladores, estén al tanto de los problemas potenciales inspeccionando los registros.

Si está utilizando un marco de registro como log4net o log4j, puede configurar una salida de registro especial adicional (también conocida como ) que le envíe por correo los errores y errores fatales. para que te mantengas informado .

[actualizar]

si el usuario final de la página no debe estar al tanto del error, puede configurar un appender  Que te envíe errores y errores mortales, estarás informado.

Si está bien que el usuario final de la página vea mensajes de error crípticos, puede configurar un appender que coloca el registro de errores para el procesamiento de la página al final de la página.

No importa cómo use el registro, si se traga la excepción, el programa continúa con datos que tienen sentido y la deglución ya no es más silenciosa.

    
respondido por el k3b 07.05.2012 - 09:38
-1

Ya hay buenas respuestas a esta pregunta, puede ser una adición a ellas: prefiero las aserciones en mi código. Si su código puede funcionar solo con objetos válidos, pero con nulo, debe establecer un valor nulo.

Las afirmaciones en este tipo de situación permitirían que cualquier prueba de unidad y / o integración falle con el mensaje exacto, por lo que puede resolver el problema bastante rápido antes de la producción. Si un error de este tipo se desliza a través de las pruebas y pasa a producción (donde deberían desactivarse las aserciones), recibirá un NpEx y un error grave, pero es mejor, que un error menor que es mucho más difuso y aparece en otro lugar en el código, y sería más caro de arreglar.

Más aún, si alguien debe trabajar con las aserciones de su código, le informa sobre las suposiciones que realizó durante el diseño / escritura de su código (en este caso: ¡Hey, este objeto debe presentarse!), lo que lo hace más fácil de mantener.

    
respondido por el GeT 11.05.2012 - 08:47
-1

Normalmente verifico si hay nulos, pero "manejo" nulos inesperados al lanzar una excepción que brinda un poco más de detalles sobre dónde ocurrió exactamente el nulo.

Editar: si obtienes un nulo cuando no esperas que algo esté mal, el programa debería morir.

    
respondido por el Loren Pechtel 11.05.2012 - 19:53
-1

Depende de la implementación del lenguaje de las excepciones. Las excepciones le permiten tomar algunas medidas para evitar daños a los datos o liberar recursos de manera limpia en caso de que su sistema entre en un estado o condición no anticipada. Esto es diferente del manejo de errores, que son condiciones que puede anticipar. Mi filosofía es que debes tener la menor excepción posible en el manejo. Es ruido ¿Puede su método hacer algo razonable manejando la excepción? ¿Se puede recuperar? ¿Se puede reparar o prevenir daños mayores? Si solo va a capturar la excepción y luego volver del método o fallar de alguna otra manera inofensiva, entonces realmente no tenía sentido capturar la excepción. Solo habría agregado ruido y complejidad a su método, y es posible que esté ocultando la fuente de una falla en su diseño. En este caso, dejaría que la falla se disparara y fuera atrapada por un bucle externo. Si puede hacer algo como reparar un objeto o marcarlo como dañado o liberar algún recurso crítico, como un archivo de bloqueo o cerrando un zócalo de manera limpia, este es un buen uso de las excepciones. Si realmente espera que el valor NULL se muestre con frecuencia como un caso perimetral válido, debe manejar esto en el flujo lógico normal con las declaraciones if-the-else o switch-case o lo que sea, no con un manejo de excepciones. Por ejemplo, un campo deshabilitado en un formulario se puede establecer en NULL, que es diferente de una cadena vacía que representa un campo en blanco. Esta es un área donde debe usar el buen juicio y el sentido común. Nunca he escuchado de buenas reglas de oro sólidas sobre cómo tratar este problema en cada situación.

Java falla en su implementación de manejo de excepciones con "excepciones comprobadas" donde todas las excepciones posibles que pueden ser provocadas por los objetos usados en un método deben ser capturadas o declaradas en la cláusula de "lanzamientos" del método. Es lo contrario de C ++ y Python, donde puede elegir manejar las excepciones que considere oportunas. En Java, si modifica el cuerpo de un método para incluir una llamada que pueda generar una excepción, se enfrenta a la opción de agregar un manejo explícito para una excepción que quizás no le importe agregar ruido y complejidad a su código, o debe agregue esa excepción a la cláusula de "lanzamientos" del método que está modificando, que no solo agrega ruido y desorden, sino que también cambia la firma de su método. Luego, debe ir a un código de modificación donde se utilice su método para manejar la nueva excepción que su método ahora puede aumentar o agregar la excepción a la cláusula de "lanzamiento" de esos métodos, lo que desencadena un efecto dominó de los cambios de código. El "requisito de captura o especificación" debe ser uno de los aspectos más molestos de Java. La excepción de excepción para RuntimeExceptions y la racionalización oficial de Java para este error de diseño es escasa.

El último párrafo tuvo poco que ver con responder tu pregunta. Simplemente odio Java.

- Noah

    
respondido por el Noah Spurrier 23.05.2015 - 16:18

Lea otras preguntas en las etiquetas