¿Por qué se rechazan las referencias nulas mientras se lanzan excepciones?


21

No entiendo muy bien el ataque constante de referencias nulas por parte de algunos amigos del lenguaje de programación. ¿Qué hay de malo en ellos? Si solicito acceso de lectura a un archivo que no existe, entonces estoy perfectamente feliz de obtener una excepción o una referencia nula y, sin embargo, las excepciones se consideran buenas, pero las referencias nulas se consideran malas. ¿Cuál es el razonamiento detrás de esto?



2
Algunos idiomas se bloquean en nulo mejor que otros. Para un "código administrado", como .Net / Java, una referencia nula es solo otro tipo de problema, mientras que otro código nativo podría no manejarlo con tanta gracia (no ha mencionado un lenguaje específico). Incluso en un mundo administrado, a veces quieres escribir código a prueba de fallas (¿incrustado ?, ¿armas?), Y a veces quieres quejarte lo antes posible (prueba de unidad). Ambos tipos de código podrían estar llamando a la misma biblioteca, eso sería un problema. En general, creo que ese código que intenta no dañar los sentimientos de las computadoras es una mala idea. La seguridad contra fallas es DURA de todos modos.
Trabajo

@ Job: Esto aboga por la pereza. Si sabe cómo manejar una excepción, la maneja. A veces, ese manejo puede implicar lanzar otra excepción, pero nunca debe dejar que una excepción de referencia nula no se controle. Siempre. Esa es la pesadilla de todo programador de mantenimiento; Es la excepción más inútil en todo el árbol. Solo pregunte a Stack Overflow .
Aaronaught

o para decirlo de otra manera: ¿por qué no devolver un código de algún tipo para representar información de error? Este argumento se desatará en los próximos años.
gbjbaanb

Respuestas:


24

Las referencias nulas no son "rechazadas" más que las excepciones, al menos por cualquiera que haya conocido o leído. Creo que estás malinterpretando la sabiduría convencional.

Lo malo es un intento de acceder a una referencia nula (o desreferenciar un puntero nulo, etc.). Esto es malo porque siempre indica un error; nunca haría algo como esto a propósito, y si lo está haciendo a propósito, entonces eso es aún peor, porque es lo que hace imposible distinguir el comportamiento esperado de un comportamiento incorrecto.

Hay ciertos grupos marginales por ahí que realmente odian el concepto de nulidad por alguna razón, pero como señala Ed , si no tiene nullo nilsimplemente tendrá que reemplazarlo por otra cosa, lo que podría conducir a algo peor que un bloqueo (como la corrupción de datos).

Muchos marcos, de hecho, abarcan ambos conceptos; por ejemplo, en .NET, un patrón frecuente que verá es un par de métodos, uno con el prefijo de la palabra Try(como TryGetValue). En el Trycaso, la referencia se establece en su valor predeterminado (generalmente null), y en el otro caso, se genera una excepción. No hay nada malo con ninguno de los enfoques; ambos se usan con frecuencia en entornos que los admiten.

Realmente todo depende de la semántica. Si nulles un valor de retorno válido, como en el caso general de buscar una colección, entonces devuelva null. Por otro lado, si es no un valor de retorno válido - por ejemplo, buscar un registro usando una clave principal que vino de su propia base de datos - luego regresar nullsería una mala idea porque la persona que llama no se lo esperaba y, probablemente, No lo comprobaré.

Es realmente muy simple descubrir qué semántica usar: ¿Tiene algún sentido que el resultado de una función sea indefinido? Si es así, puede devolver una referencia nula. Si no, lanza una excepción.


55
En realidad, hay idiomas que no tienen nulo o nulo y no "necesitan reemplazarlo por otro". Las referencias anulables implican que puede haber algo allí, o puede que no haya. Si simplemente requiere que el usuario verifique explícitamente si hay algo allí, ha resuelto el problema. Ver Haskell para un ejemplo de la vida real.
Johanna Larsson

3
@ErikKronberg, sí, el "error de mil millones de dólares" y todas esas tonterías, el desfile de personas trotando y afirmando que es fresco y fascinante nunca termina, es por eso que se eliminó el hilo de comentarios anterior. Estos reemplazos revolucionarios que las personas nunca dejan de mencionar son siempre una variante del Objeto nulo, la Opción o el Contrato, que en realidad no eliminan mágicamente el error lógico subyacente, simplemente difieren o lo promueven, respectivamente. De todos modos, esta es obviamente una pregunta sobre los lenguajes de programación que tienen null, así que realmente, Haskell es bastante irrelevante aquí.
Aaronaught

1
¿Estás argumentando seriamente que no requerir una prueba de nulo es tan bueno como exigirlo?
Johanna Larsson

3
@ErikKronberg: Sí, estoy "discutiendo seriamente" que tener que probar nulo no es particularmente diferente de (a) tener que diseñar cada nivel de una aplicación alrededor del comportamiento de un Objeto nulo, (b) tener que hacer coincidir el patrón Opciones todo el tiempo, o (c) no permitir que la persona que llama diga "No sé" y forzar una excepción o bloqueo. Hay una razón por la que nullha resistido tan bien durante tanto tiempo, y la mayoría de las personas que dicen lo contrario parecen ser académicos con poca experiencia en el diseño de aplicaciones del mundo real con restricciones como requisitos incompletos o consistencia eventual.
Aaronaught

3
@Aaronaught: A veces desearía que hubiera un botón de voto negativo para comentarios. No hay razón para despotricar así.
Michael Shaw

11

La gran diferencia es que si deja de lado el código para manejar NULL, su código continuará posiblemente bloqueándose en una etapa posterior con algún mensaje de error no relacionado, donde, como con las excepciones, la excepción se generaría en el punto inicial de falla (apertura un archivo para leer en tu ejemplo).


44
Si no se maneja un NULL, se ignoraría intencionadamente la interfaz entre su código y lo que sea que lo haya devuelto. Los desarrolladores que cometerían ese error cometerían otros en un lenguaje que no hace NULL.
Blrfl

@Blrfl, idealmente, los métodos son bastante cortos y, por lo tanto, es fácil descubrir exactamente dónde está ocurriendo el problema. Un buen depurador generalmente puede cazar bien una excepción de referencia nula, incluso si el código es largo. ¿Qué se supone que debo hacer si intento leer una configuración crítica de un registro que no está allí? Mi registro está en mal estado y estoy mejor de fallar y molestar al usuario, que recrear silenciosamente un nodo y configurarlo como predeterminado. ¿Qué pasa si un virus hizo esto? Entonces, si obtengo un valor nulo, ¿debo lanzar una excepción especializada o simplemente dejar que se rompa? Con métodos cortos, ¿cuál es la gran diferencia?
Trabajo

@ Job: ¿Qué pasa si no tienes un depurador? ¿Te das cuenta de que el 99,99% de las veces tu aplicación se ejecutará en un entorno de lanzamiento? Cuando eso suceda, desearás haber usado una excepción más significativa. Es posible que su aplicación aún tenga que fallar y molestar al usuario, pero al menos generará información de depuración que le permitirá rastrear rápidamente el problema, manteniendo así dicha molestia al mínimo.
Aaronaught

@Birfl, a veces no quiero manejar el caso donde sería nulo regresar un nulo. Por ejemplo, supongamos que tengo un contenedor que asigna claves a valores. Si mi lógica garantiza que nunca intento leer valores que no almacené primero, entonces nunca debería obtener un valor nulo. En ese caso, preferiría tener una excepción que proporcione tanta información como sea posible para indicar qué salió mal, en lugar de devolver un valor nulo para fallar misteriosamente en otro lugar del programa.
Winston Ewert

Para decirlo de otra manera, con excepciones tengo que manejar explícitamente el caso inusual o, de lo contrario, mi programa desaparece de inmediato. Con referencias nulas si el código no maneja explícitamente el caso inusual, intentará cojear. Creo que es mejor tomar el caso de fallar ahora.
Winston Ewert

8

Porque los valores nulos no son una parte necesaria de un lenguaje de programación y son una fuente constante de errores. Como usted dice, abrir un archivo puede provocar un error, que podría comunicarse de nuevo como un valor de retorno nulo o mediante una excepción. Si no se permitieran valores nulos, entonces hay una forma coherente y singular de comunicar la falla.

Además, este no es el problema más común con los nulos. La mayoría de las personas recuerda comprobar si hay nulo después de llamar a una función que puede devolverla. El problema surge mucho más en su propio diseño al permitir que las variables sean nulas en varios puntos de la ejecución de su programa. Puede diseñar su código de manera que los valores nulos nunca estén permitidos, pero si no se permitieran nulos a nivel de idioma, nada de esto sería necesario.

Sin embargo, en la práctica aún necesitaría alguna forma de indicar si una variable se inicializa o no. Entonces tendría una forma de error en el que su programa no se bloquea, sino que continúa usando algún valor predeterminado posiblemente no válido. Sinceramente, no sé cuál es mejor. Por mi dinero me gusta colapsar temprano y con frecuencia.


Considere una subclase sin inicializar con una cadena de identificación, y todos sus métodos arrojan excepciones. Si alguna de estas aparece, ya sabes lo que pasó. En sistemas embebidos con memoria limitada, la versión de producción de la fábrica sin inicializar puede volver nula.
Jim Balter

3
@Jim Balter: Sin embargo, supongo que estoy confundido sobre cómo realmente ayudaría en la práctica. En cualquier programa no trivial, en algún momento tendrá que lidiar con valores que pueden no inicializarse. Por lo tanto, debe haber alguna forma de indicar un valor predeterminado. Como tal, aún debe verificar esto antes de continuar. Entonces, en lugar de un posible bloqueo, ahora está trabajando potencialmente con datos no válidos.
Ed S.

1
También sabe lo que sucedió cuando obtuvo nullun valor de retorno: la información que solicitó no existe. No hay diferencia De cualquier manera, la persona que llama tiene que validar el valor de retorno si realmente necesita usar esa información para algún propósito en particular. Si se trata de validar un nullobjeto nulo o una mónada, no hay diferencia práctica.
Aaronaught

1
@Jim Balter: Correcto, todavía no veo la diferencia práctica, y no veo cómo hace la vida más fácil para las personas que escriben programas reales fuera de la academia. No significa que no haya beneficios, simplemente que no me parecen evidentes.
Ed S.

1
He explicado la diferencia práctica con una clase No inicializada dos veces: identifica el origen del elemento nulo, lo que permite identificar el error cuando se produce una desreferencia de nulo. En cuanto a los lenguajes diseñados alrededor de paradigmas nulos, evitan el problema desde el primer momento: proporcionan formas alternativas de programar. Eso evita variables no inicializadas; eso puede parecer difícil de entender si no está familiarizado con ellos. La programación estructurada, que también evita una gran clase de errores, también se consideraba "académica".
Jim Balter

5

Tony Hoare, quien creó la idea de una referencia nula en primer lugar, lo llama su error de un millón de dólares .

El problema no se trata de referencias nulas per-se, sino de la falta de una verificación de tipo adecuada en la mayoría de los idiomas seguros (de lo contrario).

Esta falta de soporte, desde el lenguaje, significa que los errores "errores nulos" pueden estar al acecho en el programa durante mucho tiempo antes de ser detectados. Tal es la naturaleza de los errores, por supuesto, pero ahora se sabe que los "errores nulos" son evitables.

Este problema está especialmente presente en C o C ++ (por ejemplo) debido al error "duro" que causa (un bloqueo del programa, inmediato, sin recuperación elegante).

En otros idiomas, siempre está la cuestión de cómo manejarlos.

En Java o C #, obtendría una excepción si intenta invocar un método en una referencia nula, y podría estar bien. Y, por lo tanto, la mayoría de los programadores de Java o C # están acostumbrados a esto y no entienden por qué uno querría hacer lo contrario (y reírse de C ++).

En Haskell, debe proporcionar explícitamente una acción para el caso nulo y, por lo tanto, los programadores de Haskell se regodean con sus colegas, porque lo hicieron bien (¿verdad?).

Es, en realidad, el viejo debate sobre el código de error / excepción, pero esta vez con un Sentinel Value en lugar del código de error.

Como siempre, lo que sea más apropiado realmente depende de la situación y la semántica que está buscando.


Precisamente. nullRealmente es malo, porque subvierte el sistema de tipos . (De acuerdo, la alternativa tiene un inconveniente en su verbosidad, al menos en algunos idiomas.)
Caracol mecánico

2

No puede adjuntar un mensaje de error legible por humanos a un puntero nulo.

(Sin embargo, puede dejar un mensaje de error en un archivo de registro).

En algunos lenguajes / entornos que permiten la aritmética de punteros, si uno de los argumentos de puntero es nulo y se deja en el cálculo, el resultado sería una inválida , no nulo puntero. (*) Más poder para ti.

(*) Esto sucede mucho en la programación COM , donde si intenta llamar a un método de interfaz pero el puntero de la interfaz es nulo, daría lugar a una llamada a una dirección no válida que es casi, pero no del todo, exactamente diferente a cero.


2

Devolver NULL (o cero numérico, o booleano falso) para indicar un error es incorrecto tanto técnica como conceptualmente.

Técnicamente, está cargando al programador con la comprobación inmediata del valor de retorno , en el punto exacto donde se devuelve. Si abre veinte archivos seguidos y la señalización de error se realiza devolviendo NULL, entonces el código consumidor debe verificar cada archivo leído individualmente, y salir de cualquier bucle y construcciones similares. Esta es una receta perfecta para el código desordenado. Sin embargo, si elige señalar el error lanzando una excepción, el código consumidor puede optar por manejar la excepción de inmediato, o dejar que aumente tantos niveles como sea apropiado, incluso a través de llamadas de función. Esto hace que el código sea mucho más limpio.

Conceptualmente, si abre un archivo y algo sale mal, entonces devolver un valor (incluso NULL) está mal. No tiene nada que devolver, porque su operación no terminó. Devolver NULL es el equivalente conceptual de "He leído con éxito el archivo, y esto es lo que contiene: nada". Si eso es lo que desea expresar (es decir, si NULL tiene sentido como resultado real de la operación en cuestión), devuelva NULL por todos los medios, pero si desea señalar un error, use excepciones.

Históricamente, los errores se informaron de esta manera porque los lenguajes de programación como C no tienen un manejo de excepciones integrado en el lenguaje, y la forma recomendada (usando saltos largos) es un poco complicada y un poco intuitiva.

También hay un aspecto de mantenimiento del problema: con excepciones, debe escribir un código adicional para manejar la falla; si no lo hace, el programa fallará temprano y duro (lo cual es bueno). Si devuelve NULL para señalar errores, el comportamiento predeterminado del programa es ignorar el error y continuar, hasta que conduzca a otros problemas en el futuro: datos corruptos, segfaults, NullReferenceExceptions, según el idioma. Para señalar el error temprano y en voz alta, debe escribir un código adicional y adivinar qué: esa es la parte que se deja de lado cuando está en una fecha límite ajustada.


1

Como ya se señaló, muchos idiomas no convierten la desreferenciación de un puntero nulo en una excepción capturable. Hacer eso es un truco relativamente moderno. Cuando se reconoció por primera vez el problema del puntero nulo, aún no se habían inventado excepciones.

Si permite punteros nulos como un caso válido, ese es un caso especial. Necesita lógica de manejo de casos especiales, a menudo en muchos lugares diferentes. Esa es una complejidad extra.

Ya sea que se relacione con punteros potencialmente nulos o no, si no usa lanzamientos de excepción para manejar casos excepcionales, debe manejar esos casos excepcionales de otra manera. Por lo general, cada llamada a la función debe tener comprobaciones para esos casos excepcionales, ya sea para evitar que la llamada a la función se llame inapropiadamente o para detectar el caso de falla cuando la función sale. Esa es una complejidad adicional que se puede evitar utilizando excepciones.

Más complejidad generalmente significa más errores.

Las alternativas al uso de punteros nulos en las estructuras de datos (por ejemplo, para marcar el inicio / final de una lista vinculada) incluyen el uso de elementos centinela. Estos pueden proporcionar la misma funcionalidad con mucha menos complejidad. Sin embargo, puede haber otras formas de gestionar la complejidad. Una forma es envolver el puntero potencialmente nulo en una clase de puntero inteligente para que las comprobaciones nulas solo sean necesarias en un lugar.

¿Qué hacer con el puntero nulo cuando se detecta? Si no puede incorporar el manejo de casos excepcionales, siempre puede lanzar una excepción y delegar efectivamente ese manejo de casos especiales a la persona que llama. Y eso es exactamente lo que hacen algunos idiomas de forma predeterminada cuando se desreferencia un puntero nulo.


1

Específico para C ++, pero las referencias nulas se evitan allí porque la semántica nula en C ++ está asociada con los tipos de puntero. Es bastante razonable que una función de apertura de archivo falle y devuelva un puntero nulo; de hecho, la fopen()función hace exactamente eso.


De hecho, si te encuentras con una referencia nula en C ++, es porque tu programa ya está roto .
Kaz Dragon

1

Depende del idioma.

Por ejemplo, Objective-C le permite enviar un mensaje a un objeto nulo (nulo) sin problemas. Una llamada a nil devuelve nil también y se considera una función de idioma.

Personalmente me gusta ya que puedes confiar en ese comportamiento y evitar todas esas if(obj == null)construcciones anidadas enrevesadas .

Por ejemplo:

if (myObject != nil && [myObject doSomething])
{
    ...
}

Se puede acortar a:

if ([myObject doSomething])
{
    ...
}

En resumen, hace que su código sea más legible.


0

No me importa si devuelves un valor nulo o lanzas una excepción, siempre y cuando lo documentas.


Y también nómbralo : GetAddressMaybeo GetSpouseNameOrNull.
rwong

-1

Una referencia nula generalmente ocurre porque se perdió algo en la lógica del programa, es decir: llegó a una línea de código sin pasar por la configuración requerida para ese bloque de código.

Por otro lado, si arroja una excepción por algo, significa que reconoció que podría ocurrir una situación particular en el funcionamiento normal del programa, y ​​se está manejando.


2
Una referencia nula puede "ocurrir" por una gran cantidad de razones, muy pocas de ellas relacionadas con cualquier cosa que se pierda. Quizás también lo esté confundiendo con una excepción de referencia nula .
Aaronaught

-1

Las referencias nulas suelen ser muy útiles: por ejemplo, un elemento en una lista vinculada puede tener algún sucesor o no. Usar nullpara "no sucesor" es perfectamente natural. O bien, una persona puede tener un cónyuge o no: usar nullpara "la persona no tiene cónyuge" es perfectamente natural, mucho más natural que tener un valor especial "no tiene cónyuge" al que el Person.Spousemiembro podría referirse.

Pero: muchos valores no son opcionales. En un programa OOP típico, diría que más de la mitad de las referencias no pueden ser nullposteriores a la inicialización, o el programa fallará. De lo contrario, el código tendría que estar lleno de if (x != null)cheques. Entonces, ¿por qué cada referencia debe ser anulable por defecto? Realmente debería ser al revés: las variables no deberían ser anulables de forma predeterminada, y debería decir explícitamente "oh, y este valor también puede serlo null".


¿Hay algo de esta discusión que le gustaría agregar a su respuesta? Este hilo de comentarios se calentó un poco, y nos gustaría limpiar esto. Cualquier discusión extendida debe llevarse al chat .

En medio de todas esas disputas, podría haber uno o dos puntos de interés real. Lamentablemente, no vi el comentario de Mark hasta que eliminé la discusión extendida. En el futuro, marque su respuesta para la atención de los moderadores si desea conservar los comentarios hasta que tenga tiempo de revisarlos y editar su respuesta adecuadamente.
Josh K

-1

Su pregunta está redactada de manera confusa. ¿Se refiere a una excepción de referencia nula (que en realidad es causada por un intento de eliminarreferencia nula)? La razón clara para no querer ese tipo de excepción es que no le brinda información sobre lo que salió mal, o incluso cuándo: el valor podría haberse establecido en nulo en cualquier punto del programa. Escribe "Si solicito acceso de lectura a un archivo que no existe, entonces estoy perfectamente feliz de obtener una excepción o una referencia nula", pero no debería estar perfectamente feliz de obtener algo que no indique la causa . En ninguna parte de la cadena "se hizo un intento de desreferenciar nulo" hay alguna mención de lectura o de archivos no existentes. Tal vez quiere decir que estaría perfectamente feliz de ser nulo como valor de retorno de una llamada de lectura; es un asunto bastante diferente, pero aún no le da información sobre por qué falló la lectura; eso'


No, no me refiero a una excepción de referencia nula. En todos los idiomas sé que las variables no inicializadas pero declaradas son alguna forma de nulo.
davidk01

Pero su pregunta no era sobre variables no inicializadas. Y hay lenguajes que no usan nulo para variables no inicializadas, sino que usan objetos envolventes que pueden contener opcionalmente un valor: Scala y Haskell, por ejemplo.
Jim Balter

1
"... pero en su lugar utiliza objetos de contenedor que opcionalmente pueden contener un valor ". Que claramente no es nada como un tipo anulable .
Aaronaught

1
En haskell no existe una variable no inicializada. Potencialmente, podría declarar un IORef y sembrarlo con None como valor inicial, pero eso es más o menos el análogo de declarar una variable en otro idioma y dejarla sin inicializar, lo que trae consigo los mismos problemas. Trabajando en el núcleo puramente funcional fuera de la IO monad, los programadores de haskell no recurren a los tipos de referencia, por lo que no hay un problema de referencia nulo.
davidk01

1
Si tiene un valor excepcional "Falta", eso es exactamente lo mismo que un valor "nulo" excepcional, si tiene las mismas herramientas disponibles para manejarlo. Ese es un significativo "si". Necesita complejidad adicional para manejar ese caso de cualquier manera. En Haskell, la coincidencia de patrones y el sistema de tipos proporcionan una forma de ayudar a gestionar esa complejidad. Sin embargo, existen otras herramientas para gestionar la complejidad en otros idiomas. Las excepciones son una de esas herramientas.
Steve314
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.