¿Son realmente malas las referencias nulas?


161

He oído decir que la inclusión de referencias nulas en lenguajes de programación es el "error de mil millones de dólares". ¿Pero por qué? Claro, pueden causar NullReferenceExceptions, pero ¿y qué? Cualquier elemento del lenguaje puede ser una fuente de errores si se usa incorrectamente.

¿Y cuál es la alternativa? Supongo que en lugar de decir esto:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Podrías decir esto:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

¿Pero cómo es eso mejor? De cualquier manera, si olvida verificar que el cliente existe, obtendrá una excepción.

Supongo que una CustomerNotFoundException es un poco más fácil de depurar que una NullReferenceException en virtud de ser más descriptiva. Eso es todo?


54
Sería cauteloso con el enfoque "si el Cliente existe / luego obtenga el Cliente", tan pronto como escriba dos líneas de código como ese, estará abriendo la puerta a las condiciones de la carrera. Obtener, y luego buscar nulos / excepciones de captura es más seguro.
Carson63000

26
Si solo pudiéramos eliminar la fuente de todos los errores de tiempo de ejecución, ¡nuestro código estaría garantizado para ser perfecto! Si las referencias NULL en C # te rompen el cerebro, prueba los punteros inválidos, pero no NULL en C;)
LnxPrgr3

Sin embargo, existen operadores de navegación segura y de fusión nula en algunos idiomas
jk.

35
nulo per se no es malo. Un sistema de tipos que no hace diferencia entre el tipo T y el tipo T + nulo es malo.
Ingo

27
Volviendo a mi propia pregunta unos años más tarde, ahora soy totalmente un converso del enfoque Opción / Quizás.
Tim Goodman

Respuestas:


91

nulo es malo

Hay una presentación sobre InfoQ sobre este tema: Referencias nulas: el error del billón de dólares por Tony Hoare

Tipo de opción

La alternativa de la programación funcional es usar un tipo de Opción , que puede contener SOME valueo NONE.

Un buen artículo El Patrón de "Opción" que discute el tipo de Opción y proporciona una implementación para Java.

También he encontrado un informe de error para Java sobre este problema: agregue tipos de opciones agradables a Java para evitar NullPointerExceptions . La característica solicitada se introdujo en Java 8.


55
Nullable <x> en C # es un concepto similar, pero no llega a ser una implementación del patrón de opción. La razón principal es que en .NET System.Nullable se limita a los tipos de valor.
MattDavey

27
+1 para el tipo de opción. Después de familiarizarse con el Quizás de Haskell , los nulos comienzan a verse raros ...
Andres F.

55
El desarrollador puede cometer errores en cualquier lugar, por lo que en lugar de una excepción de puntero nulo, obtendrá una excepción de opción vacía. ¿Cómo es mejor lo último que lo primero?
Greenoldman

77
@greenoldman no existe tal cosa como "excepción de opción vacía". Intente insertar valores NULL en las columnas de la base de datos definidas como NOT NULL.
Jonas

36
@greenoldman El problema con los nulos es que cualquier cosa puede ser nula y, como desarrollador, debes ser muy cauteloso. Un Stringtipo no es realmente una cadena. Es un String or Nulltipo Los lenguajes que se adhieren completamente a la idea de los tipos de opción no permiten valores nulos en su sistema de tipos, lo que garantiza que si define una firma de tipo que requiere un String(o cualquier otra cosa), debe tener un valor. El Optiontipo está ahí para dar una representación a nivel de tipo de cosas que pueden o no tener un valor, para que sepa dónde debe manejar explícitamente esas situaciones.
KChaloux

127

El problema es que, debido a que en teoría cualquier objeto puede ser nulo y arrojar una excepción cuando intentas usarlo, tu código orientado a objetos es básicamente una colección de bombas sin explotar.

Tiene razón en que el manejo elegante de errores puede ser funcionalmente idéntico a las ifdeclaraciones de verificación nula . Pero, ¿qué sucede cuando algo de lo que te has convencido a ti mismo de que no podría ser nulo es, de hecho, un nulo? Kerboom Pase lo que pase después, estoy dispuesto a apostar que 1) no será elegante y 2) no le gustará.

Y no descarte el valor de "fácil de depurar". El código de producción maduro es una criatura loca y en expansión; cualquier cosa que le brinde más información sobre lo que salió mal y dónde puede ahorrarle horas de excavación.


27
+1 Por lo general, los errores en el código de producción DEBEN identificarse lo más rápido posible para que puedan repararse (por lo general, error de datos). Cuanto más fácil sea depurar, mejor.

44
Esta respuesta es la misma si se aplica a cualquiera de los ejemplos del OP. O prueba las condiciones de error o no, y las maneja con gracia o no lo hace. En otras palabras, eliminar referencias nulas del lenguaje no cambia las clases de errores que experimentará, solo cambiará sutilmente la manifestación de esos errores.
dash-tom-bang

19
+1 para bombas sin explotar. Con referencias nulas, decir public Foo doStuff()no significa "hacer cosas y devolver el resultado de Foo" significa "hacer cosas y devolver el resultado de Foo, O devolver nulo, pero no tienes idea de si eso sucederá o no". El resultado es que tiene que marcar nulo POR TODAS PARTES para estar seguro. También podría eliminar los valores nulos y utilizar un tipo o patrón especial (como un tipo de valor) para indicar un valor sin sentido.
Matt Olenik

12
@greenoldman, su ejemplo es doblemente efectivo para demostrar nuestro punto en que el uso de tipos primitivos (ya sean nulos o enteros) como modelos semánticos es una mala práctica. Si tiene un tipo para el que los números negativos no son una respuesta válida, debe crear una nueva clase de tipos para implementar el modelo semántico con ese significado. Ahora, todo el código para manejar el modelado de ese tipo no negativo se localiza en su clase, aislado del resto del sistema, y ​​cualquier intento de crear un valor negativo para él puede detectarse de inmediato.
Huperniketes

99
@greenoldman, continúa demostrando que los tipos primitivos son inadecuados para el modelado. Primero fue sobre números negativos. Ahora está sobre el operador de división cuando cero es el divisor. Si sabe que no puede dividir por cero, puede predecir que el resultado no será bonito y es responsable de asegurarse de realizar la prueba y proporcionar el valor "válido" apropiado para ese caso. Los desarrolladores de software son responsables de conocer los parámetros en los que opera su código y de codificarlos adecuadamente. Es lo que distingue a la ingeniería de los retoques.
Huperniketes

50

Hay varios problemas con el uso de referencias nulas en el código.

Primero, generalmente se usa para indicar un estado especial . En lugar de definir una nueva clase o constante para cada estado, ya que las especializaciones se realizan normalmente, usar una referencia nula es usar un tipo / valor generalizado masivo y con pérdida .

En segundo lugar, el código de depuración se vuelve más difícil cuando aparece una referencia nula e intenta determinar qué lo generó , qué estado está vigente y su causa, incluso si puede rastrear su ruta de ejecución ascendente.

Tercero, las referencias nulas introducen rutas de código adicionales para probar .

Cuarto, una vez que las referencias nulas se utilizan como estados válidos para los parámetros, así como los valores de retorno, la programación defensiva (para los estados causados ​​por el diseño) requiere más verificación de referencias nulas en varios lugares ... por si acaso.

Quinto, el tiempo de ejecución del lenguaje ya está realizando verificaciones de tipo cuando realiza una búsqueda de selector en la tabla de métodos de un objeto. Entonces, está duplicando el esfuerzo al verificar si el tipo de objeto es válido / no válido y luego hacer que el tiempo de ejecución verifique el tipo de objeto válido para invocar su método.

¿Por qué no utilizar el patrón NullObject para aprovechar la verificación del tiempo de ejecución para que invoque métodos NOP específicos de ese estado (conforme a la interfaz del estado normal) y al mismo tiempo eliminar todas las comprobaciones adicionales de referencias nulas en toda su base de código?

Que implica más trabajo mediante la creación de una clase NullObject para cada interfaz con la que se desea representar un estado especial. Pero al menos la especialización está aislada para cada estado especial, en lugar del código en el que el estado podría estar presente. IOW, el número de pruebas se reduce porque tiene menos rutas de ejecución alternativas en sus métodos.


77
... porque averiguar quién generó (o mejor dicho, no se molestó en cambiar o inicializar) los datos basura que llenan su objeto supuestamente útil es mucho más fácil que rastrear una referencia NULL. No lo compro, aunque en su mayoría estoy atascado en tierras estáticamente tipificadas.
dash-tom-bang

Sin embargo, los datos basura son mucho más raros que las referencias nulas. Particularmente en lenguajes gestionados.
Dean Harding

55
-1 para objeto nulo.
DeadMG

44
Los puntos (4) y (5) son sólidos, pero el NullObject no es un remedio. Caerá en una trampa aún peor: errores de lógica (flujo de trabajo). Cuando conozca EXACTAMENTE la situación actual, seguro, puede ayudar, pero usarlo a ciegas (en lugar de nulo) le costará más.
Greenoldman

1
@ gnasher729, aunque el tiempo de ejecución de Objective-C no arrojará una excepción de puntero nulo como lo hacen Java y otros sistemas, no hace que el uso de referencias nulas sea mejor por las razones que describí en mi respuesta. Por ejemplo, si no hay navigationController en [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];el tiempo de ejecución lo trata como una secuencia de no-ops, la vista no se elimina de la pantalla y puede sentarse frente a Xcode rascándose la cabeza durante horas tratando de averiguar por qué.
Huperniketes

48

Los nulos no son tan malos, a menos que no los esperes. Usted debe necesitar especificar explícitamente en el código que usted está esperando nulo, lo que es un problema del lenguaje de diseño. Considera esto:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Si intenta invocar métodos en un Customer?, debería obtener un error en tiempo de compilación. La razón por la que más idiomas no hacen esto (IMO) es que no permiten que el tipo de variable cambie según el ámbito en el que se encuentre. Si el idioma puede manejar eso, entonces el problema puede resolverse completamente dentro del sistema de tipos

También existe la forma funcional de manejar este problema, usando tipos de opciones y Maybe, pero no estoy tan familiarizado con él. Prefiero esta manera porque el código correcto debería teóricamente solo necesitar un carácter agregado para compilar correctamente.


11
Wow, eso es genial. Además de detectar muchos más errores en el momento de la compilación, me ahorra tener que verificar la documentación para determinar si GetByLastNamedevuelve nulo si no se encuentra (en lugar de lanzar una excepción). Solo puedo ver si regresa Customer?o no Customer.
Tim Goodman el

14
@JBRWilkinson: Este es solo un ejemplo elaborado para mostrar la idea, no un ejemplo de una implementación real. De hecho, creo que es una buena idea.
Dean Harding

1
Sin embargo, tienes razón en que no es el patrón de objeto nulo.
Dean Harding

13
Esto se parece a lo que Haskell llama a Maybe- A Maybe Customeres Just c(donde ces a Customer) o Nothing. En otros idiomas, se llama Option: vea algunas de las otras respuestas a esta pregunta.
MatrixFrog

2
@SimonBarker: puede estar seguro si Customer.FirstNamees de tipo String(en oposición a String?). Ese es el beneficio real: solo las variables / propiedades que tienen un valor significativo de nada deben declararse como nulables.
Yogu

20

Hay muchas respuestas excelentes que cubren los síntomas desafortunados de null, por lo que me gustaría presentar un argumento alternativo: Null es una falla en el sistema de tipos.

El propósito de un sistema de tipos es asegurar que los diferentes componentes de un programa "encajen" adecuadamente; un programa bien escrito no puede "descarrilarse" en un comportamiento indefinido.

Considere un dialecto hipotético de Java, o cualquiera que sea su lenguaje de tipo estático preferido, donde puede asignar la cadena "Hello, world!"a cualquier variable de cualquier tipo:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

Y puede verificar variables de esta manera:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

No hay nada imposible en esto: alguien podría diseñar ese lenguaje si quisiera. El valor especial no tiene que ser "Hello, world!"- que podría haber sido el número 42, la tupla (1, 4, 9), o, por ejemplo, null. ¿Pero por qué harías esto? Una variable de tipo Foosolo debe contener Foos, ¡ese es el punto principal del sistema de tipos! nullNo es Foomás de lo que "Hello, world!"es. Peor aún, nullno es un valor de ningún tipo, ¡y no hay nada que puedas hacer con él!

El programador nunca puede estar seguro de que una variable realmente contenga un Foo, y tampoco puede hacerlo el programa; Para evitar un comportamiento indefinido, debe verificar las variables "Hello, world!"antes de usarlas como Foos. Tenga en cuenta que hacer la verificación de cadena en el fragmento anterior no propaga el hecho de que foo1 es realmente un Foo- barprobablemente también tendrá su propia verificación, solo para estar seguro.

Compare eso con el uso de un Maybe/ Optiontipo con coincidencia de patrones:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Dentro de la Just foocláusula, tanto usted como el programa saben con certeza que nuestra Maybe Foovariable realmente contiene un Foovalor: que la información se propaga por la cadena de llamadas y barno necesita hacer ninguna verificación. Debido a que Maybe Fooes un tipo distinto Foo, se ve obligado a manejar la posibilidad de que pueda contener Nothing, por lo que nunca puede ser sorprendido por a NullPointerException. Puede razonar sobre su programa mucho más fácilmente y el compilador puede omitir comprobaciones nulas sabiendo que todas las variables de tipo Foorealmente contienen Foos. Todos ganan.


¿Qué pasa con los valores nulos en Perl 6? Todavía son nulos, excepto que siempre se escriben. ¿Sigue siendo un problema que puedes escribir? ¿ my Int $variable = IntDónde Intestá el valor de tipo nulo Int?
Konrad Borowski

@xfix No estoy familiarizado con Perl, pero siempre y cuando usted no tiene una forma de hacer cumplir this type only contains integersen oposición a this type contains integers and this other value, tendrá un problema cada vez que sólo se desea enteros.
Doval

1
+1 para la coincidencia de patrones. Con la cantidad de respuestas anteriores que mencionan los tipos de Opción, pensaría que alguien hubiera mencionado el hecho de que una característica de lenguaje simple como los patrones de coincidencia puede hacer que sean mucho más intuitivos para trabajar que los nulos.
Jules

1
Creo que la unión monádica es aún más útil que la coincidencia de patrones (aunque, por supuesto, la vinculación para Quizás se implementa utilizando la coincidencia de patrones). Creo que es la clase de idiomas viendo divertidas como C # poniendo sintaxis especial por muchas cosas ( ??, ?.y así sucesivamente) para las cosas que lenguajes como Haskell implementan funciones como la biblioteca. Creo que es mucho más ordenado. null/ Nothingpropagación no es "especial" de ninguna manera, entonces, ¿por qué deberíamos aprender más sintaxis para tenerla?
sara

14

(Lanzando mi sombrero en el ring por una vieja pregunta;))

El problema específico con nulo es que rompe la escritura estática.

Si tengo un Thing t, entonces el compilador puede garantizar que puedo llamar t.doSomething(). Bueno, A MENOS que tsea ​​nulo en tiempo de ejecución. Ahora todas las apuestas están apagadas. El compilador dijo que estaba bien, pero descubrí mucho más tarde que tNO lo hace doSomething(). Entonces, en lugar de poder confiar en el compilador para detectar errores de tipo, tengo que esperar hasta el tiempo de ejecución para detectarlos. ¡También podría usar Python!

Entonces, en cierto sentido, nulo introduce el tipeo dinámico en un sistema tipado estáticamente, con los resultados esperados.

La diferencia entre que una división entre cero o un log negativo, etc. es que cuando i = 0, sigue siendo un int. El compilador aún puede garantizar su tipo. El problema es que la lógica aplica mal ese valor de una manera que no está permitida por la lógica ... pero si la lógica lo hace, esa es más o menos la definición de un error.

(El compilador puede detectar algunos de esos problemas, por cierto. Como marcar expresiones como i = 1 / 0en tiempo de compilación. Pero realmente no puede esperar que el compilador siga una función y se asegure de que todos los parámetros sean consistentes con la lógica de la función)

El problema práctico es que haces mucho trabajo extra y agregas controles nulos para protegerte en el tiempo de ejecución, pero ¿qué pasa si olvidas uno? El compilador le impide asignar:

Cadena s = nuevo entero (1234);

Entonces, ¿por qué debería permitir la asignación a un valor (nulo) que romperá las desreferencias s?

Al mezclar "sin valor" con referencias "escritas" en su código, está poniendo una carga adicional en los programadores. Y cuando suceden NullPointerExceptions, rastrearlas puede llevar aún más tiempo. En lugar de confiar en la escritura estática para decir "esto es una referencia a algo esperado", está dejando que el lenguaje diga "esto bien podría ser una referencia a algo esperado".


3
En C, una variable de tipo *intidentifica un int, o NULL. Del mismo modo en C ++, una variable de tipo *Widgetidentifica a Widget, una cosa cuyo tipo deriva de Widget, o NULL. El problema con Java y derivados como C # es que usan la ubicación de almacenamiento Widgetpara significar *Widget. La solución no es fingir que *Widgetno debería ser capaz de mantener NULL, sino más bien tener un Widgettipo de ubicación de almacenamiento adecuado .
supercat

14

Un nullpuntero es una herramienta.

No es un enemigo. Deberías usarlos bien.

Piense un momento acerca de cuánto tiempo lleva encontrar y corregir un error de puntero no válido típico , en comparación con localizar y corregir un error de puntero nulo. Es fácil verificar un puntero null. Es un PITA verificar si un puntero no nulo apunta o no a datos válidos.

Si aún no está convencido, agregue una buena dosis de subprocesamiento múltiple a su escenario, luego piense nuevamente.

Moraleja de la historia: no tires a los niños con el agua. Y no culpe a las herramientas por el accidente. El hombre que inventó el número cero hace mucho tiempo fue bastante inteligente, ya que ese día se podía llamar "nada". El nullpuntero no está tan lejos de eso.


EDITAR: Aunque el NullObjectpatrón parece ser una mejor solución que las referencias que pueden ser nulas, presenta problemas por sí solo:

  • Una referencia que contiene un NullObjectdebería (según la teoría) no hace nada cuando se llama a un método. Por lo tanto, se pueden introducir errores sutiles debido a referencias erróneamente no asignadas, que ahora se garantiza que no son nulas (¡sí!) Pero realizan una acción no deseada: nada. Con un NPE es obvio dónde radica el problema. Con un NullObjectcomportamiento que de alguna manera (pero incorrecto), presentamos el riesgo de que los errores se detecten (demasiado) tarde. Esto no es, por accidente, similar a un problema de puntero no válido y tiene consecuencias similares: la referencia apunta a algo que parece datos válidos, pero no lo es, introduce errores de datos y puede ser difícil de localizar. Para ser sincero, en estos casos preferiría sin pestañear un NPE que falla inmediatamente, ahora,sobre un error lógico / de datos que de repente me golpea más adelante en el camino. ¿Recuerda el estudio de IBM sobre el costo de los errores en función de en qué etapa se detectan?

  • La noción de no hacer nada cuando se llama a un método en NullObjectno se cumple, cuando se llama a un captador de propiedades o una función, o cuando el método devuelve valores a través de out. Digamos que el valor de retorno es un inty decidimos que "no hacer nada" significa "devolver 0". Pero, ¿cómo podemos estar seguros de que 0 es el "nada" correcto para ser (no) devuelto? Después de todo, el NullObjectno debe hacer nada, pero cuando se le pide un valor, tiene que reaccionar de alguna manera. Fallar no es una opción: utilizamos el NullObjectpara evitar el NPE, y seguramente no lo cambiaremos por otra excepción (no implementado, dividir por cero, ...), ¿verdad? Entonces, ¿cómo no devuelves nada correctamente cuando tienes que devolver algo?

No puedo evitarlo, pero cuando alguien intenta aplicar el NullObjectpatrón a todos y cada uno de los problemas, se parece más a intentar reparar un error haciendo otro. Es sin duda una solución buena y útil en algunos casos, pero seguramente no es la bala mágica para todos los casos.


¿Puedes presentar un argumento de por qué nulldebería existir? Es posible agitar a mano cualquier defecto de idioma con "no es un problema, simplemente no se equivoque".
Doval

¿A qué valor establecería un puntero no válido?
JensG

Bueno, no los establece en ningún valor, porque no debería permitirlos en absoluto. En su lugar, utiliza una variable de un tipo diferente, quizás llamada Maybe Tque puede contener Nothingun Tvalor o uno . La clave aquí es que se ve obligado a verificar si un contenido Mayberealmente contiene un Tantes de que se le permita usarlo, y proporcionar un curso de acción en caso de que no lo contenga . Y debido a que ha rechazado punteros inválidos, ahora nunca tiene que verificar nada que no sea un Maybe.
Doval

1
@JensG en su último ejemplo, ¿el compilador le hace hacer la verificación nula antes de cada llamada t.Something()? ¿O tiene alguna forma de saber que ya hizo la verificación y no obligarlo a hacerlo nuevamente? ¿Qué pasa si hiciste la verificación antes de pasar ta este método? Con el tipo Opción, el compilador sabe si ya ha realizado la comprobación o no al observar el tipo de t. Si es una Opción, no la ha marcado, mientras que si es el valor sin envolver, entonces sí.
Tim Goodman

1
¿Qué devuelve el objeto nulo cuando se le solicita un valor?
JensG

8

Las referencias nulas son un error porque permiten código no sensorial:

foo = null
foo.bar()

Existen alternativas, si aprovecha el sistema de tipos:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

La idea general es colocar la variable en un cuadro, y lo único que puede hacer es desempaquetarla, preferiblemente alistando la ayuda del compilador como se propone para Eiffel.

Haskell lo tiene desde cero ( Maybe), en C ++ puede aprovechar, boost::optional<T>pero aún puede obtener un comportamiento indefinido ...


66
-1 para "apalancamiento"!
Mark C

8
Pero seguramente muchas construcciones de programación comunes permiten código sin sentido, ¿no? La división por cero es (posiblemente) sin sentido, pero todavía permitimos la división, y esperamos que el programador recuerde verificar que el divisor no sea cero (o manejar la excepción).
Tim Goodman el

3
No estoy seguro de lo que entendemos por "partir de cero", pero Maybeno está integrado en el núcleo del lenguaje, su definición sólo pasa a ser en el preludio estándar: data Maybe t = Nothing | Just t.
fredoverflow

44
@FredOverflow: dado que el preludio se carga por defecto, lo consideraría "desde cero", que Haskell tiene un sistema de tipos lo suficientemente sorprendente como para permitir que esta (y otras) sutilezas se definan con las reglas generales del idioma es solo una ventaja :)
Matthieu M.

2
@TimGoodman Si bien la división por cero puede no ser el mejor ejemplo para todos los tipos de valores numéricos (IEEE 754 define un resultado para la división por cero), en los casos en que arrojaría una excepción, en lugar de tener valores de tipo Tdivididos por otros valores de tipo T, limitaría la operación a valores de tipo Tdivididos por valores de tipo NonZero<T>.
kbolino

8

¿Y cuál es la alternativa?

Tipos opcionales y coincidencia de patrones. Como no conozco C #, aquí hay un código en un lenguaje ficticio llamado Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}

6

El problema con los nulos es que los lenguajes que les permiten te obligan a programar defensivamente contra él. Se requiere mucho esfuerzo (mucho más que tratar de usar bloqueos defensivos) para asegurarse de que

  1. los objetos que espera que no sean nulos nunca son nulos, y
  2. que sus mecanismos defensivos realmente abordan todos los NPE potenciales de manera efectiva.

Entonces, de hecho, los nulos terminan siendo algo costoso.


1
Puede ser útil si incluye un ejemplo en el que se requiere mucho más esfuerzo para tratar con los valores nulos. En mi ejemplo ciertamente simplificado, el esfuerzo es casi el mismo de cualquier manera. No estoy en desacuerdo con usted, solo encuentro ejemplos de código (pseudo) específicos que pintan una imagen más clara que las declaraciones generales.
Tim Goodman el

2
Ah, no me expliqué claramente. No es que sea más difícil lidiar con nulos. Es que es extremadamente difícil (si no imposible) garantizar que todo el acceso a referencias potencialmente nulas ya esté protegido. Es decir, en un lenguaje que permite referencias nulas, es simplemente imposible garantizar que un código de tamaño arbitrario esté libre de excepciones de puntero nulo, no sin algunas dificultades y esfuerzos importantes. Eso es lo que hace que las referencias nulas sean algo costoso. Es extremadamente costoso garantizar la ausencia total de excepciones de puntero nulo.
luis.espinal

4

La cuestión de hasta qué punto su lenguaje de programación intenta probar la corrección de su programa antes de ejecutarlo. En un lenguaje de tipo estático, demuestra que tiene los tipos correctos. Al pasar al valor predeterminado de referencias no anulables (con referencias anulables opcionales) puede eliminar muchos de los casos en los que se pasa nulo y no debería ser así. La pregunta es si el esfuerzo adicional en el manejo de referencias no anulables vale la pena en términos de corrección del programa.


4

El problema no es tanto nulo, es que no se puede especificar un tipo de referencia no nulo en muchos idiomas modernos.

Por ejemplo, su código podría verse así

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Cuando se transmiten las referencias de clase, la programación defensiva lo alienta a verificar todos los parámetros para ver si son nulos nuevamente, incluso si acaba de verificarlos como nulos de antemano, especialmente al construir unidades reutilizables bajo prueba. ¡La misma referencia podría comprobarse nula fácilmente 10 veces en la base de código!

¿No sería mejor si pudieras tener un tipo anulable normal cuando obtienes la cosa, tratar con nulo en ese momento y luego, pasarlo como un tipo no anulable a todas tus pequeñas funciones y clases de ayuda sin verificar nulos todos ¿hora?

Ese es el punto de la solución Opción: no permitirle activar estados de error, sino permitir el diseño de funciones que implícitamente no pueden aceptar argumentos nulos. Si la implementación "predeterminada" de tipos es anulable o no anulable es menos importante que la flexibilidad de tener ambas herramientas disponibles.

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Esta también figura como la característica más votada # 2 para C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c


Creo que otro punto importante sobre la Opción <T> es que es una mónada (y, por lo tanto, también un endofunctor), por lo que puede componer cálculos complejos sobre flujos de valor que contienen tipos opcionales sin verificar explícitamente Nonecada paso del camino.
Sara

3

Nulo es malo. Sin embargo, la falta de un nulo puede ser un mal mayor.

El problema es que en el mundo real a menudo tienes una situación en la que no tienes datos. Su ejemplo de la versión sin nulos aún puede explotar: o cometió un error lógico y olvidó buscar a Goodman o tal vez Goodman se casó entre cuando lo revisó y cuando la buscó. (Ayuda a evaluar la lógica si crees que Moriarty está mirando cada parte de tu código y tratando de hacerte tropezar).

¿Qué hace la búsqueda de Goodman cuando ella no está allí? Sin un valor nulo, debe devolver algún tipo de cliente predeterminado, y ahora está vendiendo cosas a ese valor predeterminado.

Básicamente, se trata de si es más importante que el código funcione sin importar qué o si funciona correctamente. Si el comportamiento inapropiado es preferible a ningún comportamiento, entonces no querrá valores nulos. (Para ver un ejemplo de cómo esta podría ser la elección correcta, considere el primer lanzamiento del Ariane V. Un error no detectado / 0 provocó que el cohete diera un giro difícil y la autodestrucción se disparara al desarmarse debido a esto. El valor estaba tratando de calcular que en realidad ya no sirvió para nada en el instante en que se encendió el amplificador, habría puesto en órbita a pesar de la rutina que devolvía la basura).

Al menos 99 de cada 100 veces elegiría usar nulos. Sin embargo, saltaría de alegría al notar la versión de ellos.


1
Esta es una buena respuesta. La construcción nula en la programación no es el problema, sino todas las instancias de valores nulos. Si crea un objeto predeterminado, eventualmente todos sus valores nulos serán reemplazados por esos valores predeterminados y su UI, DB y cualquier lugar intermedio se rellenarán con esos, que probablemente ya no sean útiles. Pero podrás dormir por la noche porque supongo que al menos tu programa no falla.
Chris McCall

2
Asume que esa nulles la única (o incluso preferible) forma de modelar la ausencia de un valor.
Sara

1

Optimizar para el caso más común.

Tener que verificar nulo todo el tiempo es tedioso: desea poder obtener el objeto Cliente y trabajar con él.

En el caso normal, esto debería funcionar bien: realiza la búsqueda, obtiene el objeto y lo usa.

En el caso excepcional , cuando está buscando (aleatoriamente) un cliente por su nombre, sin saber si ese registro / objeto existe, necesitaría alguna indicación de que esto falló. En esta situación, la respuesta es lanzar una excepción RecordNotFound (o dejar que el proveedor de SQL a continuación haga esto por usted).

Si se encuentra en una situación en la que no sabe si puede confiar en los datos que ingresan (el parámetro), tal vez porque fueron ingresados ​​por un usuario, entonces también podría proporcionar el 'TryGetCustomer (nombre, cliente externo)' modelo. Referencia cruzada con int.Parse e int. TryParse.


Yo afirmaría que nunca se sabe si puede confiar en los datos que ingresan porque están llegando a una función y son de tipo anulable. El código podría ser refactorizado para hacer la entrada totalmente diferente. Entonces, si hay un nulo posible, siempre debes verificarlo. Así que terminas con un sistema donde cada variable se verifica para nulo cada vez antes de acceder. Los casos en los que no está marcado se convierten en el porcentaje de falla de su programa.
Chris McCall

0

¡Las excepciones no se evalúan!

Están ahí por una razón. Si su código está funcionando mal, hay un patrón genial, llamado excepción , y le dice que algo está mal.

Al evitar el uso de objetos nulos, está ocultando parte de esas excepciones. No estoy hablando del ejemplo OP donde convierte la excepción de puntero nulo en una excepción bien escrita, esto podría ser algo bueno ya que aumenta la legibilidad. Sin embargo, cuando toma la solución de tipo Opción como señaló @Jonas, está ocultando excepciones.

Que en su aplicación de producción, en lugar de que se produzca una excepción al hacer clic en el botón para seleccionar una opción vacía, no sucede nada. En lugar de una excepción de puntero nulo se generaría y probablemente obtendríamos un informe de esa excepción (como en muchas aplicaciones de producción tenemos dicho mecanismo), y de lo que podríamos solucionarlo.

Hacer que su código sea a prueba de balas evitando excepciones es una mala idea, hacer que su código sea a prueba de balas arreglando excepciones, bueno, este es el camino que elegiría.


1
Sé bastante más sobre el tipo de opción ahora que cuando publiqué la pregunta hace unos años, ya que ahora los uso regularmente en Scala. El punto de Opción es que hace que la asignación Nonede algo que no sea una Opción en un error en tiempo de compilación, y de la misma manera, use un Optioncomo si tuviera un valor sin verificar primero que es un error en tiempo de compilación. Los errores de compilación son ciertamente mejores que las excepciones de tiempo de ejecución.
Tim Goodman

Por ejemplo: no puedes decir s: String = None, tienes que decir s: Option[String] = None. Y si ses una Opción, no puede decir s.length, sin embargo, puede verificar que tenga un valor y extraer el valor al mismo tiempo, con coincidencia de patrones:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman

Desafortunadamente en Scala todavía se puede decir s: String = null, pero ese es el precio de la interoperabilidad con Java. Generalmente no permitimos ese tipo de cosas en nuestro código Scala.
Tim Goodman

2
Sin embargo, estoy de acuerdo con el comentario general de que las excepciones (usadas correctamente) no son algo malo, y "arreglar" una excepción al tragarla generalmente es una idea terrible. Pero con el tipo Opción, el objetivo es convertir las excepciones en errores en tiempo de compilación, que son algo mejor.
Tim Goodman

@TimGoodman, ¿es solo una táctica de escala o se puede usar en otros lenguajes como Java y ActionScript 3? También sería bueno si pudieras editar tu respuesta con un ejemplo completo.
Ilya Gazman

0

Nullsson problemáticos porque deben verificarse explícitamente, pero el compilador no puede advertirle que olvidó verificarlos. Solo el análisis estático que lleva mucho tiempo puede decirle eso. Afortunadamente, hay varias buenas alternativas.

Saca la variable del alcance. Con demasiada frecuencia, nullse usa como un marcador de posición cuando un programador declara una variable demasiado pronto o la mantiene demasiado tiempo. El mejor enfoque es simplemente deshacerse de la variable. No lo declare hasta que tenga un valor válido para poner allí. Esta no es una restricción tan difícil como podría pensar, y hace que su código sea mucho más limpio.

Usa el patrón de objeto nulo. A veces, un valor faltante es un estado válido para su sistema. El patrón de objeto nulo es una buena solución aquí. Evita la necesidad de verificaciones explícitas constantes, pero aún así le permite representar un estado nulo. No se trata de "ocultar una condición de error", como afirman algunas personas, porque en este caso un estado nulo es un estado semántico válido. Dicho esto, no debe usar este patrón cuando un estado nulo no es un estado semántico válido. Simplemente debe sacar su variable fuera del alcance.

Use un Quizás / Opción. En primer lugar, esto permite que el compilador le advierta que necesita verificar un valor faltante, pero hace más que reemplazar un tipo de verificación explícita con otro. Utilizando Options, puede encadenar su código y continuar como si su valor existiera, sin necesidad de verificarlo hasta el último minuto absoluto. En Scala, su código de ejemplo se vería así:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

En la segunda línea, donde estamos construyendo el mensaje impresionante, no hacemos una verificación explícita si se encontró al cliente, y la belleza es que no necesitamos hacerlo . No es hasta la última línea, que podría ser muchas líneas más tarde, o incluso en un módulo diferente, donde explícitamente nos preocupamos por lo que sucede si Optiones así None. No solo eso, si nos hubiéramos olvidado de hacerlo, no habría marcado el tipo. Incluso entonces, se hace de una manera muy natural, sin una ifdeclaración explícita .

Compare eso con un null, donde escribe las comprobaciones bien, pero donde tiene que hacer una verificación explícita en cada paso del camino o toda su aplicación explota en el tiempo de ejecución, si tiene suerte durante un caso de uso, su unidad prueba el ejercicio. Simplemente no vale la pena.


0

El problema central de NULL es que hace que el sistema no sea confiable. En 1980 Tony Hoare en el periódico dedicado a su Premio Turing escribió:

Y así, el mejor de mis consejos para los creadores y diseñadores de ADA ha sido ignorado. ... No permita que este lenguaje en su estado actual se use en aplicaciones donde la confiabilidad es crítica, es decir, centrales nucleares, misiles de crucero, sistemas de alerta temprana, sistemas de defensa antimisiles antibalísticos. El próximo cohete que se extravía como resultado de un error del lenguaje de programación puede no ser un cohete espacial exploratorio en un viaje inofensivo a Venus: puede ser una ojiva nuclear explotando sobre una de nuestras propias ciudades. Un lenguaje de programación poco confiable que genera programas poco confiables constituye un riesgo mucho mayor para nuestro medio ambiente y para nuestra sociedad que los automóviles inseguros, los pesticidas tóxicos o los accidentes en las centrales nucleares. Esté atento para reducir el riesgo, no para aumentarlo.

El lenguaje ADA ha cambiado mucho desde entonces, sin embargo, tales problemas todavía existen en Java, C # y muchos otros lenguajes populares.

Es deber del desarrollador crear contratos entre un cliente y un proveedor. Por ejemplo, en C #, como en Java, puede usar Genericspara minimizar el impacto de la Nullreferencia creando solo lectura NullableClass<T>(dos Opciones):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

y luego úsalo como

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

o use dos opciones de estilo con métodos de extensión C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

La diferencia es significativa. Como cliente de un método, sabe qué esperar. Un equipo puede tener la regla:

Si una clase no es de tipo NullableClass, entonces su instancia no debe ser nula .

El equipo puede fortalecer esta idea utilizando Diseño por contrato y verificación estática en el momento de la compilación, por ejemplo, con la condición previa:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

o para una cuerda

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Este enfoque puede aumentar drásticamente la confiabilidad de la aplicación y la calidad del software. Design by Contract es un caso de lógica Hoare , que fue poblada por Bertrand Meyer en su famoso libro de Construcción de Software Orientado a Objetos y lenguaje Eiffel en 1988, pero no se usa de manera inválida en la creación moderna de software.


0

No.

El hecho de que un idioma no nulltenga en cuenta un valor especial como es el problema del idioma. Si observa idiomas como Kotlin o Swift , que obligan al escritor a abordar explícitamente la posibilidad de un valor nulo en cada encuentro, entonces no hay peligro.

En lenguajes como la que nos ocupa, el lenguaje permite nullser un valor de cualquier -ish tipo, pero no proporciona ningún construcciones para reconocer que más tarde, lo que lleva a las cosas que son nullde forma inesperada (especialmente cuando se pasa a usted de algún otro lugar), y por lo tanto, se bloquea. Ese es el mal: dejarte usar nullsin hacerte lidiar con eso.


-1

Básicamente, la idea central es que los problemas de referencia de puntero nulo deben quedar atrapados en el momento de la compilación en lugar del tiempo de ejecución. Entonces, si está escribiendo una función que toma algún objeto y no desea que nadie lo llame con referencias nulas, entonces el sistema de tipos debería permitirle especificar ese requisito para que el compilador pueda usarlo para garantizar que la llamada a su función nunca compilar si la persona que llama no cumple con sus requisitos. Esto se puede hacer de muchas maneras, por ejemplo, decorando sus tipos o usando tipos de opciones (también llamados tipos anulables en algunos idiomas), pero obviamente eso es mucho más trabajo para los diseñadores de compiladores.

Creo que es comprensible por qué en los años sesenta y setenta, el diseñador de compiladores no hizo todo lo posible para implementar esta idea porque las máquinas tenían recursos limitados, los compiladores debían ser relativamente simples y nadie sabía realmente qué tan malo sería esto. años más adelante.


-1

Tengo una simple objeción contra null:

Rompe la semántica de su código por completo al introducir ambigüedad .

A menudo espera que el resultado sea de cierto tipo. Esto es semánticamente sólido. Digamos que usted le pidió a la base de datos un usuario con cierto id, espera que el resultado sea de cierto tipo (= user). Pero, ¿qué pasa si no hay usuario de esa identificación? Una solución (mala) es: agregar un nullvalor. Entonces, el resultado es ambiguo : es un usero es null. Pero nullno es el tipo esperado. Y aquí es donde comienza el olor del código .

Al evitarlo null, deja su código semánticamente claro.

Siempre hay formas de nullevitarlo: refactorizar a colecciones Null-objects, optionalsetc.


-2

Si será semánticamente posible acceder a una ubicación de almacenamiento de referencia o tipo de puntero antes de ejecutar el código para calcular un valor para él, no hay una elección perfecta de lo que debería suceder. Tener tales ubicaciones predeterminadas a referencias de puntero nulo que se pueden leer y copiar libremente, pero que fallarán si se desreferencian o indexan, es una opción posible. Otras opciones incluyen:

  1. Tener ubicaciones predeterminadas para un puntero nulo, pero no permita que el código las vea (o incluso determine que son nulas) sin fallar. Posiblemente proporcione un medio explícito para establecer un puntero en nulo.
  2. Haga que las ubicaciones tengan un puntero nulo predeterminado y se bloquee si se intenta leer uno, pero proporcione un medio sin fallas para probar si un puntero se mantiene nulo. También proporcione un medio explícito de establecer un puntero en nulo.
  3. Tener ubicaciones predeterminadas para un puntero a alguna instancia predeterminada proporcionada por el compilador particular del tipo indicado.
  4. Tener ubicaciones predeterminadas para un puntero a alguna instancia predeterminada proporcionada por el programa particular del tipo indicado.
  5. Tener acceso de puntero nulo (no solo operaciones de desreferenciación) llame a alguna rutina proporcionada por el programa para proporcionar una instancia.
  6. Diseñe la semántica del lenguaje de modo que no existan colecciones de ubicaciones de almacenamiento, excepto un compilador temporal inaccesible, hasta que se hayan ejecutado rutinas de inicialización en todos los miembros (algo como un constructor de matriz tendría que proporcionarse con una instancia predeterminada, una función que devolvería una instancia, o posiblemente un par de funciones: una para devolver una instancia y otra que se llamaría en instancias construidas previamente si se produce una excepción durante la construcción de la matriz).

La última opción podría tener algún atractivo, especialmente si un lenguaje incluye tipos anulables y no anulables (uno podría llamar a los constructores de matrices especiales para cualquier tipo, pero solo se requiere llamarlos al crear matrices de tipos no anulables), pero probablemente no hubiera sido factible cuando se inventaron los punteros nulos. De las otras opciones, ninguna parece más atractiva que permitir la copia de punteros nulos, pero no desreferenciarlos o indexarlos. El enfoque n. ° 4 puede ser conveniente para tener como opción, y debería ser bastante económico de implementar, pero ciertamente no debería ser la única opción. Requerir que los punteros deben apuntar de manera predeterminada a algún objeto válido en particular es mucho peor que tener punteros predeterminados a un valor nulo que puede leerse o copiarse pero no desreferenciarse o indexarse.


-2

Sí, NULL es un diseño terrible , en un mundo orientado a objetos. En pocas palabras, el uso NULL conduce a:

  • manejo de errores ad-hoc (en lugar de excepciones)
  • semántica ambigua
  • falla lenta en lugar de rápida
  • pensamiento de computadora versus pensamiento de objeto
  • objetos mutables e incompletos

Consulte esta publicación de blog para obtener una explicación detallada: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

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.