Atribución: Esto surgió de una pregunta relacionada con P.SE
Mi experiencia es en C / C ++, pero he trabajado bastante en Java y actualmente estoy codificando C #. Debido a mi experiencia en C, la verificación de los punteros pasados y devueltos es de segunda mano, pero reconozco que sesga mi punto de vista.
Hace poco vi una mención al Patrón de objetos nulos donde la idea es que siempre se devuelve un objeto. El caso normal devuelve el objeto poblado esperado y el caso de error devuelve un objeto vacío en lugar de un puntero nulo. La premisa es que la función de llamada siempre tendrá algún tipo de objeto para acceder y, por lo tanto, evitará violaciones de memoria de acceso nulo.
- Entonces, ¿cuáles son las ventajas y desventajas de una verificación nula frente al uso del patrón de objeto nulo?
Puedo ver un código de llamada más limpio con el NOP, pero también puedo ver dónde crearía fallas ocultas que de otro modo no se generarían. Prefiero que mi aplicación falle con dificultad (también conocida como una excepción) mientras la estoy desarrollando antes que tener un error silencioso para escapar a la naturaleza.
- ¿No puede el patrón de objetos nulos tener problemas similares a los de no realizar una verificación nula?
Muchos de los objetos con los que he trabajado contienen objetos o contenedores propios. Parece que tendría que tener un caso especial para garantizar que todos los contenedores del objeto principal tuvieran objetos vacíos. Parece que esto podría ponerse feo con múltiples capas de anidamiento.
fuente
Respuestas:
No usaría un Patrón de objeto nulo en lugares donde se devuelve nulo (u Objeto nulo) porque hubo una falla catastrófica. En esos lugares continuaría volviendo nulo. En algunos casos, si no hay recuperación, también podría bloquearse porque al menos el volcado de bloqueo indicará exactamente dónde ocurrió el problema. En tales casos, cuando agrega su propio manejo de errores, todavía va a matar el proceso (nuevamente, dije para los casos en que no hay recuperación), pero su manejo de errores enmascarará información muy importante que un volcado por caída habría proporcionado.
El patrón de objeto nulo es más para lugares donde hay un comportamiento predeterminado que podría tomarse en un caso donde no se encuentra el objeto. Por ejemplo, considere lo siguiente:
Si usa NOP, escribiría:
Tenga en cuenta que el comportamiento de este código es "si Bob existe, quiero actualizar su dirección". Obviamente, si su aplicación requiere que Bob esté presente, no desea tener éxito en silencio. Pero hay casos en los que este tipo de comportamiento sería apropiado. Y en esos casos, ¿NOP no produce un código mucho más limpio y conciso?
En lugares donde realmente no puede vivir sin Bob, haría que GetUser () lanzara una excepción de aplicación (es decir, no una infracción de acceso ni nada por el estilo) que se manejaría en un nivel superior e informaría un fallo general de la operación. En este caso, no hay necesidad de NOP, pero tampoco hay necesidad de verificar explícitamente NULL. OMI, esas comprobaciones de NULL, solo hacen que el código sea más grande y eliminan la legibilidad. Verificar NULL sigue siendo la opción de diseño correcta para algunas interfaces, pero no tanto como algunas personas tienden a pensar.
fuente
Pros
Contras
Este último "profesional" es el principal diferenciador (en mi experiencia) en cuanto a cuándo se debe aplicar cada uno. "¿Debería la falla ser ruidosa?". En algunos casos, desea que un fracaso sea duro e inmediato; si algún escenario que nunca debería suceder de alguna manera lo hace. Si no se encontró un recurso vital ... etc. En algunos casos, está de acuerdo con un valor predeterminado sensato, ya que no es realmente un error : obtener un valor de un diccionario, pero falta la clave, por ejemplo.
Al igual que cualquier otra decisión de diseño, existen ventajas y desventajas según sus necesidades.
fuente
No soy una buena fuente de información objetiva, pero subjetivamente:
En un entorno como Java o C #, no le dará el beneficio principal de lo explícito: la seguridad de la solución está completa, porque aún puede recibir nulo en lugar de cualquier objeto en el sistema y Java no tiene medios (excepto las anotaciones personalizadas para Beans , etc.) para protegerte de eso, excepto los if explícitos. Pero en el sistema libre de implementaciones nulas, abordar la solución del problema de tal manera (con objetos que representan casos excepcionales) brinda todos los beneficios mencionados. Por lo tanto, intente leer sobre algo distinto de los idiomas principales, y se encontrará cambiando sus formas de codificación.
En general, los objetos Nulo / Error / tipos de retorno se consideran una estrategia de manejo de errores muy sólida y matemáticamente sólida, mientras que la estrategia de manejo de excepciones de Java o C va solo un paso por encima de la estrategia de "morir lo antes posible", así que básicamente deje toda la carga de inestabilidad e imprevisibilidad del sistema en desarrollador o mantenedor.
También hay mónadas que se pueden considerar superiores a esta estrategia (también superiores en complejidad de comprensión al principio), pero se trata más de los casos en los que necesita modelar aspectos externos de tipo, mientras que Objeto nulo modela aspectos internos. Entonces NonExistingUser probablemente esté mejor modelado como monad maybe_existing.
Y, por último, no creo que haya ninguna conexión entre el patrón de objeto nulo y el manejo silencioso de situaciones de error. Debería ser al revés: debería forzarlo a modelar cada caso de error explícitamente y manejarlo en consecuencia. Ahora, por supuesto, puede decidir ser descuidado (por cualquier razón) y omitir manejar el caso de error explícitamente, pero manejarlo genéricamente, bueno, esa fue su elección explícita.
fuente
Creo que es interesante observar que la viabilidad de un objeto nulo parece ser un poco diferente dependiendo de si su idioma está estáticamente o no. Ruby es un lenguaje que implementa un objeto para Null (o
Nil
en la terminología del lenguaje).En lugar de recuperar un puntero a la memoria no inicializada, el lenguaje devolverá el objeto
Nil
. Esto se ve facilitado por el hecho de que el lenguaje se escribe dinámicamente. No se garantiza que una función siempre devuelva unint
. Puede regresarNil
si eso es lo que necesita hacer. Se vuelve más complicado y difícil hacer esto en un lenguaje estáticamente tipado, porque naturalmente espera que nulo sea aplicable a cualquier objeto que pueda llamarse por referencia. Tendría que comenzar a implementar versiones nulas de cualquier objeto que pudiera ser nulo (o ir a la ruta Scala / Haskell y tener algún tipo de envoltorio "Quizás").MrLister mencionó el hecho de que en C ++ no se garantiza que tenga una referencia / puntero de inicialización cuando lo cree por primera vez. Al tener un objeto nulo dedicado en lugar de un puntero nulo sin procesar, puede obtener algunas características adicionales agradables. Ruby
Nil
incluye una funciónto_s
(toString) que da como resultado un texto en blanco. Esto es excelente si no le importa obtener un retorno nulo en una cadena y desea omitir el informe sin fallar. Las clases abiertas también le permiten anular manualmente la salida deNil
si es necesario.De acuerdo, todo esto se puede tomar con un grano de sal. Creo que en un lenguaje dinámico, el objeto nulo puede ser una gran conveniencia cuando se usa correctamente. Desafortunadamente, aprovecharlo al máximo puede requerir que edite su funcionalidad básica, lo que podría hacer que su programa sea difícil de entender y mantener para las personas acostumbradas a trabajar con sus formularios más estándar.
fuente
Para obtener un punto de vista adicional, eche un vistazo a Objective-C, donde en lugar de tener un objeto nulo, el valor nil funciona como un objeto. Cualquier mensaje enviado a un objeto nulo no tiene efecto y devuelve un valor de 0 / 0.0 / NO (falso) / NULL / nil, cualquiera que sea el tipo de valor de retorno del método. Esto se usa como un patrón muy generalizado en la programación de MacOS X e iOS, y generalmente los métodos se diseñarán de manera que un objeto nulo que devuelva 0 sea un resultado razonable.
Por ejemplo, si desea verificar si una cadena tiene uno o más caracteres, puede escribir
y obtenga un resultado razonable si aString == nil, porque una cadena no existente no contiene ningún carácter. La gente tiende a tomarse un tiempo para acostumbrarse, pero probablemente me tomaría un tiempo acostumbrarme a verificar valores nulos en otros idiomas.
(También hay un objeto singleton de clase NSNull que se comporta de manera bastante diferente y no se usa mucho en la práctica).
fuente
nil
llegar#define
a NULL y se comporta como un objeto nulo. Esa es una característica de tiempo de ejecución mientras que en C # / Java los punteros nulos emiten errores.No lo use tampoco si puede evitarlo. Utilice una función de fábrica que devuelva un tipo de opción, una tupla o un tipo de suma dedicada. En otras palabras, represente un valor potencialmente predeterminado con un tipo diferente de un valor garantizado que no debe ser predeterminado.
Primero, enumeremos los desiderata, luego pensemos cómo podemos expresar esto en algunos lenguajes (C ++, OCaml, Python)
grep
busquen posibles errores.Creo que la tensión entre el Patrón de objetos nulos y los punteros nulos proviene de (5). Sin embargo, si podemos detectar errores lo suficientemente temprano, (5) se vuelve discutible.
Consideremos este idioma por idioma:
C ++
En mi opinión, una clase de C ++ generalmente debería ser construible por defecto, ya que facilita las interacciones con las bibliotecas y facilita el uso de la clase en contenedores. También simplifica la herencia ya que no necesita pensar a qué constructor de superclase llamar.
Sin embargo, esto significa que no puede saber con certeza si un valor de tipo
MyClass
está en el "estado predeterminado" o no. Además de poner en unbool nonempty
campo o similar a la superficie predeterminada en tiempo de ejecución, lo mejor que puede hacer es producir nuevas instancias deMyClass
una manera que obligue al usuario a verificarlo .Recomiendo usar una función de fábrica que devuelva a
std::optional<MyClass>
,std::pair<bool, MyClass>
o una referencia de valor r a astd::unique_ptr<MyClass>
si es posible.Si desea que su función de fábrica devuelva algún tipo de estado de "marcador de posición" que sea distinto de un estado predeterminado
MyClass
, usestd::pair
ay asegúrese de documentar que su función lo hace.Si la función de fábrica tiene un nombre único, es fácil
grep
para el nombre y buscar descuido. Sin embargo, es difícilgrep
para los casos en que el programador debería haber utilizado la función de fábrica pero no lo hizo .OCaml
Si está utilizando un lenguaje como OCaml, puede usar un
option
tipo (en la biblioteca estándar de OCaml) o uneither
tipo (no en la biblioteca estándar, pero fácil de usar ). O undefaultable
tipo (estoy inventando el términodefaultable
).A
defaultable
como se muestra arriba es mejor que un par porque el usuario debe combinar patrones para extraer el'a
y no puede simplemente ignorar el primer elemento del par.El equivalente del
defaultable
tipo que se muestra arriba se puede usar en C ++ usando astd::variant
con dos instancias del mismo tipo, pero partes de lastd::variant
API no se pueden usar si ambos tipos son iguales. También es un uso extrañostd::variant
ya que sus constructores tipo no tienen nombre.Pitón
De todos modos, no obtiene ninguna comprobación de tiempo de compilación para Python. Pero, al escribir de forma dinámica, generalmente no hay circunstancias en las que necesite una instancia de marcador de posición de algún tipo para aplacar el compilador.
Recomendaría simplemente lanzar una excepción cuando se vea obligado a crear una instancia predeterminada.
Si eso no es aceptable, recomendaría crear uno
DefaultedMyClass
que heredeMyClass
y devolverlo de su función de fábrica. Eso le da flexibilidad en términos de funcionalidad de "desconexión" en casos predeterminados si es necesario.fuente