Si nulo es malo, ¿por qué los idiomas modernos lo implementan? [cerrado]

82

Estoy seguro de que los diseñadores de lenguajes como Java o C # conocían problemas relacionados con la existencia de referencias nulas (consulte ¿Son realmente malas las referencias nulas? ). Además, implementar un tipo de opción no es realmente mucho más complejo que las referencias nulas.

¿Por qué decidieron incluirlo de todos modos? Estoy seguro de que la falta de referencias nulas alentaría (o incluso forzaría) un código de mejor calidad (especialmente un mejor diseño de biblioteca) tanto de los creadores de lenguaje como de los usuarios.

¿Se debe simplemente al conservadurismo: "otros idiomas lo tienen, nosotros también debemos tenerlo ..."?

mrpyo
fuente
99
nulo es genial. Me encanta y lo uso todos los días.
Pieter B
17
@PieterB ¿Pero lo usa para la mayoría de las referencias, o desea que la mayoría de las referencias no sean nulas? El argumento no es que no debe haber datos anulables, solo que debe ser explícito y participar.
11
@PieterB Pero cuando la mayoría no debe ser anulable, ¿no tendría sentido hacer que la capacidad nula sea la excepción en lugar de la predeterminada? Tenga en cuenta que si bien el diseño habitual de los tipos de opciones es forzar la comprobación explícita de ausencia y desempaquetado, también se puede tener la conocida semántica Java / C # / ... para referencias nulables opcionales (usar como si no es nulable, explotar) si es nulo) Al menos evitaría algunos errores y haría un análisis estático que se queje de la falta de controles nulos mucho más práctico.
20
¿Qué pasa con ustedes, chicos? De todas las cosas que pueden salir mal del software, intentar desreferenciar un valor nulo no es ningún problema. SIEMPRE genera un AV / segfault y así se repara. ¿Hay tanta escasez de errores que tiene que preocuparse por esto? Si es así, tengo mucho de repuesto, y ninguno de ellos invoca problemas con referencias nulas / punteros.
Martin James
13
@MartinJames "SIEMPRE genera un AV / segfault y así se arregla" - no, no, no lo hace.
detly

Respuestas:

97

Descargo de responsabilidad: dado que no conozco personalmente a ningún diseñador de idiomas, cualquier respuesta que le dé será especulativa.

Del propio Tony Hoare :

Lo llamo mi error de mil millones de dólares. Fue la invención de la referencia nula en 1965. En ese momento, estaba diseñando el primer sistema de tipo integral para referencias en un lenguaje orientado a objetos (ALGOL W). Mi objetivo era asegurar que todo uso de referencias debería ser absolutamente seguro, con una verificación realizada automáticamente por el compilador. Pero no pude resistir la tentación de poner una referencia nula, simplemente porque era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallas en el sistema, lo que probablemente ha causado miles de millones de dólares de dolor y daños en los últimos cuarenta años.

El énfasis es mío.

Naturalmente, no le pareció una mala idea en ese momento. Es probable que se haya perpetuado en parte por la misma razón: si le pareció una buena idea al inventor de Quicksort ganador del Premio Turing, no es sorprendente que muchas personas todavía no entiendan por qué es malo. También es probable en parte porque es conveniente que los nuevos idiomas sean similares a los idiomas antiguos, tanto por razones de marketing como de aprendizaje. Caso en punto:

"Estábamos detrás de los programadores de C ++. Logramos arrastrar a muchos de ellos a mitad de camino a Lisp". -Guy Steele, coautor de la especificación Java

(Fuente: http://www.paulgraham.com/icad.html )

Y, por supuesto, C ++ tiene nulo porque C tiene nulo, y no hay necesidad de entrar en el impacto histórico de C. C # reemplazó a J ++, que fue la implementación de Java de Microsoft, y también reemplazó a C ++ como el lenguaje elegido para el desarrollo de Windows, por lo que podría haberlo obtenido de cualquiera de los dos.

EDITAR Aquí hay otra cita de Hoare que vale la pena considerar:

Los lenguajes de programación en general son mucho más complicados de lo que solían ser: la orientación a objetos, la herencia y otras características todavía no se están considerando desde el punto de vista de una disciplina coherente y científicamente bien fundada o una teoría de la corrección. . Mi postulado original, que he estado siguiendo como científico toda mi vida, es que uno usa los criterios de corrección como un medio para converger en un diseño de lenguaje de programación decente, uno que no establezca trampas para sus usuarios, y otros en que los diferentes componentes del programa corresponden claramente a los diferentes componentes de su especificación, por lo que puede razonar compositivamente al respecto. [...] Las herramientas, incluido el compilador, deben basarse en alguna teoría de lo que significa escribir un programa correcto. -Entrevista de historia oral de Philip L. Frana, 17 de julio de 2002, Cambridge, Inglaterra; Instituto Charles Babbage, Universidad de Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Nuevamente, el énfasis es mío. Sun / Oracle y Microsoft son empresas, y el resultado final de cualquier empresa es el dinero. Los beneficios para ellos de haber tenido nullpueden haber superado las desventajas, o simplemente haber tenido una fecha límite demasiado ajustada para considerar completamente el problema. Como ejemplo de un error de lenguaje diferente que probablemente ocurrió debido a los plazos:

Es una pena que Cloneable esté roto, pero sucede. Las API de Java originales se realizaron muy rápidamente en un plazo ajustado para cumplir con una ventana de mercado de cierre. El equipo original de Java hizo un trabajo increíble, pero no todas las API son perfectas. Cloneable es un punto débil, y creo que las personas deberían ser conscientes de sus limitaciones. -Josh Bloch

(Fuente: http://www.artima.com/intv/bloch13.html )

Doval
fuente
32
Estimado votante: ¿cómo puedo mejorar mi respuesta?
Doval
66
En realidad no respondiste la pregunta; solo proporcionó algunas citas sobre algunas opiniones posteriores y algunos comentarios adicionales sobre el "costo". (Si es un error nulo mil millones de dólares, no debe el dinero ahorrado por la EM y Java mediante la implementación de lo que reducir la deuda?)
DougM
29
@DougM ¿Qué esperas que haga, contactar a todos los diseñadores de idiomas de los últimos 50 años y preguntarle por qué lo implementó nullen su idioma? Cualquier respuesta a esta pregunta será especulativa a menos que provenga de un diseñador de idiomas. No conozco ninguno que frecuenta este sitio además de Eric Lippert. La última parte es un arenque rojo por numerosas razones. La cantidad de código de terceros escrito sobre las API de MS y Java obviamente supera la cantidad de código en la propia API. Entonces, si sus clientes quieren null, se los dan null. También supones que han aceptado nullque les está costando dinero.
Doval
3
Si la única respuesta que puede dar es especulativa, dígalo claramente en su párrafo inicial. (Usted preguntó cómo se podría mejorar su respuesta, y me respondió Cualquier paréntesis no es más que el comentario puede sentirse libre de ignorar; eso es lo que entre paréntesis son en Inglés, después de todo..)
DougM
77
Esta respuesta es razonable; He agregado algunas consideraciones más en la mía. Observo que ICloneableestá igualmente roto en .NET; Desafortunadamente, este es un lugar donde las deficiencias de Java no se aprendieron a tiempo.
Eric Lippert
121

Estoy seguro de que los diseñadores de lenguajes como Java o C # conocían problemas relacionados con la existencia de referencias nulas

Por supuesto.

Además, implementar un tipo de opción no es realmente mucho más complejo que las referencias nulas.

¡Siento disentir! Las consideraciones de diseño que entraron en los tipos de valores anulables en C # 2 fueron complejas, controvertidas y difíciles. Llevaron a los equipos de diseño de los lenguajes y el tiempo de ejecución muchos meses de debate, implementación de prototipos, etc.

¿Por qué decidieron incluirlo de todos modos?

Todo diseño es un proceso de elección entre muchas metas sutiles y extremadamente incompatibles; Solo puedo dar un breve resumen de algunos de los factores que se considerarían:

  • La ortogonalidad de las características del lenguaje generalmente se considera algo bueno. C # tiene tipos de valores anulables, tipos de valores no anulables y tipos de referencia anulables. Los tipos de referencia no anulables no existen, lo que hace que el sistema de tipos no sea ortogonal.

  • La familiaridad con los usuarios existentes de C, C ++ y Java es importante.

  • La interoperabilidad fácil con COM es importante.

  • La fácil interoperabilidad con todos los demás lenguajes .NET es importante.

  • La fácil interoperabilidad con las bases de datos es importante.

  • La consistencia de la semántica es importante; si tenemos una referencia de TheKingOfFrance igual a nulo, ¿eso siempre significa "no hay un Rey de Francia en este momento", o también puede significar "Definitivamente hay un Rey de Francia; simplemente no sé quién es en este momento"? ¿O puede significar "la idea misma de tener un Rey en Francia no tiene sentido, así que ni siquiera hagas la pregunta!" Nulo puede significar todas estas cosas y más en C #, y todos estos conceptos son útiles.

  • El costo de rendimiento es importante.

  • Ser susceptible al análisis estático es importante.

  • La consistencia del sistema de tipos es importante; ¿podemos saber siempre que una referencia no anulable nunca se considera inválida bajo ninguna circunstancia? ¿Qué pasa en el constructor de un objeto con un campo de tipo de referencia no anulable? ¿Qué pasa con el finalizador de dicho objeto, donde el objeto se finaliza porque el código que supuestamente debía completar la referencia arrojó una excepción ? Un sistema de tipos que le miente sobre sus garantías es peligroso.

  • ¿Y qué hay de la consistencia de la semántica? Los valores nulos se propagan cuando se usan, pero las referencias nulas arrojan excepciones cuando se usan. Eso es inconsistente; ¿Es esa inconsistencia justificada por algún beneficio?

  • ¿Podemos implementar la característica sin romper otras características? ¿Qué otras posibles características futuras excluye la característica?

  • Vas a la guerra con el ejército que tienes, no con el que te gustaría. Recuerde, C # 1.0 no tenía genéricos, por lo que hablar Maybe<T>como alternativa es un completo no iniciador. ¿Debería haber disminuido .NET durante dos años mientras el equipo de tiempo de ejecución agregaba genéricos, únicamente para eliminar referencias nulas?

  • ¿Qué pasa con la consistencia del sistema de tipos? Puedes decir Nullable<T>cualquier tipo de valor: no, espera, eso es mentira. No se puede decir Nullable<Nullable<T>>. ¿Deberías poder hacerlo? Si es así, ¿cuál es su semántica deseada? ¿Vale la pena hacer que todo el sistema de tipos tenga un caso especial solo para esta característica?

Y así. Estas decisiones son complejas.

Eric Lippert
fuente
12
+1 para todo, pero especialmente para mencionar genéricos. Es fácil olvidar que hubo períodos de tiempo tanto en Java como en la historia de C # donde los genéricos no existían.
Doval
2
Tal vez una pregunta tonta (solo soy un estudiante universitario de TI), pero no se pudo implementar el tipo de opción en el nivel de sintaxis (con CLR sin saber nada al respecto) como una referencia nulable regular que requiere una verificación de "valor tiene" antes de usar en ¿código? Creo que los tipos de opciones no necesitan verificaciones en tiempo de ejecución.
mrpyo
2
@mrpyo: Claro, esa es una posible opción de implementación. Ninguna de las otras opciones de diseño desaparece, y esa opción de implementación tiene muchas ventajas y desventajas propias.
Eric Lippert
1
@mrpyo Creo que forzar una verificación de "valor tiene" no es una buena idea. Teóricamente es una muy buena idea, pero en la práctica, IMO traería todo tipo de comprobaciones vacías, solo para satisfacer al compilador, como excepciones comprobadas en Java y las personas que lo engañan con catcheseso no hacen nada. Creo que es mejor dejar que el sistema explote en lugar de continuar la operación en un estado posiblemente no válido.
Nada imposible
2
@voo: Las matrices de tipo de referencia no anulable son difíciles por muchas razones. Hay muchas soluciones posibles y todas ellas imponen costos en diferentes operaciones. La sugerencia de Supercat es rastrear si un elemento puede leerse legalmente antes de ser asignado, lo que impone costos. El suyo es asegurarse de que se ejecute un inicializador en cada elemento antes de que la matriz sea visible, lo que impone un conjunto diferente de costos. Así que aquí está el problema: no importa cuál de estas técnicas elija, alguien se quejará de que no es eficiente para su escenario favorito. Estos son puntos serios en contra de la característica.
Eric Lippert
28

Nulo tiene un propósito muy válido de representar una falta de valor.

Diré que soy la persona más vocal que conozco sobre los abusos de null y todos los dolores de cabeza y sufrimiento que pueden causar, especialmente cuando se usan libremente.

Mi postura personal es que las personas pueden usar valores nulos solo cuando pueden justificar que es necesario y apropiado.

Ejemplo justificando nulos:

La fecha de fallecimiento suele ser un campo anulable. Hay tres situaciones posibles con fecha de fallecimiento. O la persona ha muerto y se conoce la fecha, la persona ha muerto y la fecha se desconoce, o la persona no está muerta y, por lo tanto, no existe una fecha de muerte.

La fecha de fallecimiento también es un campo de fecha y hora y no tiene un valor "desconocido" o "vacío". Tiene la fecha predeterminada que aparece cuando crea una nueva fecha y hora que varía según el idioma utilizado, pero técnicamente existe la posibilidad de que esa persona realmente muriera en ese momento y se marcaría como su "valor vacío" si fuera usa la fecha predeterminada.

Los datos tendrían que representar la situación correctamente.

La persona está muerta se conoce la fecha de la muerte (3/9/1984)

Simple, '3/9/1984'

La persona está muerta, se desconoce la fecha de la muerte

Entonces, ¿qué es lo mejor? Nulo , '0/0/0000' o '01 / 01/1869 '(¿o cuál es su valor predeterminado?)

La persona no está muerta, la fecha de fallecimiento no es aplicable

Entonces, ¿qué es lo mejor? Nulo , '0/0/0000' o '01 / 01/1869 '(¿o cuál es su valor predeterminado?)

Así que pensemos cada valor sobre ...

  • Nulo , tiene implicaciones y preocupaciones de las que debes tener cuidado, tratar accidentalmente de manipularlo sin confirmar que no es nulo primero, por ejemplo, arrojaría una excepción, pero también representa mejor la situación real ... Si la persona no está muerta la fecha de la muerte no existe ... no es nada ... es nula ...
  • 0/0/0000 , esto podría estar bien en algunos idiomas, e incluso podría ser una representación apropiada de ninguna fecha. Desafortunadamente, algunos idiomas y validaciones rechazarán esto como una fecha y hora no válida, lo que lo hace imposible en muchos casos.
  • 1/1/1869 (o cualquiera que sea su valor predeterminado de fecha y hora) , el problema aquí es que es difícil de manejar. Podría usarlo como su valor de falta de valor, excepto ¿qué sucede si quiero filtrar todos mis registros para los que no tengo una fecha de fallecimiento? Podría filtrar fácilmente a las personas que realmente murieron en esa fecha, lo que podría causar problemas de integridad de datos.

El hecho es que a veces te No tiene que representar nada y seguro a veces un tipo de variable que funciona bien para eso, sino que a menudo tipos variables tienen que ser capaces de representar nada.

Si no tengo manzanas, tengo 0 manzanas, pero ¿qué pasa si no sé cuántas manzanas tengo?

Por supuesto, se abusa de nulo y es potencialmente peligroso, pero a veces es necesario. Es solo el valor predeterminado en muchos casos porque hasta que proporcione un valor, la falta de un valor y algo debe representarlo. (Nulo)

RualStorge
fuente
37
Null serves a very valid purpose of representing a lack of value.Un tipo Optiono Maybesirve para este propósito muy válido sin pasar por alto el sistema de tipos.
Doval
34
Nadie argumenta que no debería haber un valor de falta de valor, argumentan que los valores que pueden faltar deberían marcarse explícitamente como tales, en lugar de que cada valor se pierda potencialmente.
2
Supongo que RualStorge estaba hablando en relación con SQL, porque hay campos que indican que cada columna debe marcarse como NOT NULL. Sin embargo, mi pregunta no estaba relacionada con RDBMS ...
mrpyo
55
+1 para distinguir entre "sin valor" y "valor desconocido"
David
2
¿No tendría más sentido separar el estado de una persona? Es decir, un Persontipo tiene un statecampo de tipo State, que es una unión discriminada de Alivey Dead(dateOfDeath : Date).
jon-hanson
10

No iría tan lejos como "otros idiomas lo tienen, tenemos que tenerlo también ..." como si fuera una especie de mantenerse al día con los Jones. Una característica clave de cualquier lenguaje nuevo es la capacidad de interoperar con las bibliotecas existentes en otros lenguajes (léase: C). Dado que C tiene punteros nulos, la capa de interoperabilidad necesita necesariamente el concepto de nulo (o algún otro equivalente "no existe" que explota cuando lo usa).

El diseñador de lenguaje podría haber elegido usar Tipos de opciones y forzarlo a manejar la ruta nula en todas partes donde las cosas podrían ser nulas. Y eso seguramente conduciría a menos errores.

Pero (especialmente para Java y C # debido al momento de su presentación y su público objetivo) el uso de tipos de opciones para esta capa de interoperabilidad probablemente habría perjudicado si no hubiera torpedeado su adopción. O bien, el tipo de opción se pasa completamente, molestando a los programadores de C ++ de mediados a finales de los 90, o la capa de interoperabilidad arrojaría excepciones al encontrar nulos, molestando a los programadores de C ++ de mediados a finales de los 90. ..

Telastyn
fuente
3
El primer párrafo no tiene sentido para mí. Java no tiene interoperabilidad C en la forma que sugieres (hay JNI pero ya salta a través de una docena de aros para todo lo relacionado con referencias; además, rara vez se usa en la práctica), lo mismo para otros lenguajes "modernos".
@delnan: lo siento, estoy más familiarizado con C #, que tiene este tipo de interoperabilidad. Más bien supuse que muchas de las bibliotecas Java fundamentales también usan JNI en la parte inferior.
Telastyn
66
Es un buen argumento para permitir nulo, pero aún puede permitir nulo sin alentarlo . Scala es un buen ejemplo de esto. Puede interoperar perfectamente con las API de Java que usan nulo, pero se recomienda que lo envuelva Optionpara usarlo en Scala, que es tan fácil como val x = Option(possiblyNullReference). En la práctica, las personas no tardan mucho en ver los beneficios de un Option.
Karl Bielefeldt
1
Los tipos de opciones van de la mano con la coincidencia de patrones (estáticamente verificada), que C # desafortunadamente no tiene. F # lo hace, y es maravilloso.
Steven Evers
1
@SteveEvers Es posible simularlo usando una clase base abstracta con un constructor privado, clases internas selladas y un Matchmétodo que tome delegados como argumentos. Luego pasa expresiones lambda a Match(puntos de bonificación por usar argumentos con nombre) y Matchllama a la correcta.
Doval
7

En primer lugar, creo que todos podemos estar de acuerdo en que es necesario un concepto de nulidad. Hay algunas situaciones en las que necesitamos representar la ausencia de información.

Permitir nullreferencias (y punteros) es solo una implementación de este concepto, y posiblemente la más popular, aunque se sabe que tiene problemas: C, Java, Python, Ruby, PHP, JavaScript, ... todos usan una similar null.

Por qué ? Bueno, cual es la alternativa?

En lenguajes funcionales como Haskell tiene el tipo Optiono Maybe; Sin embargo, estos se basan en:

  • tipos paramétricos
  • tipos de datos algebraicos

Ahora, ¿el C, Java, Python, Ruby o PHP originales admitían alguna de esas características? No. Los genéricos defectuosos de Java son recientes en la historia del lenguaje y dudo que los demás los implementen.

Ahí tienes. nullEs fácil, los tipos de datos algebraicos paramétricos son más difíciles. La gente fue por la alternativa más simple.

Matthieu M.
fuente
+1 para "nulo es fácil, los tipos de datos algebraicos paramétricos son más difíciles". Pero creo que no fue tanto un problema de mecanografía paramétrica y que los ADT eran más difíciles; es solo que no son percibidos como necesarios. Si Java se hubiera enviado sin un sistema de objetos, por otro lado, habría fracasado; OOP era una función de "showtopping", en el sentido de que si no la tenía, nadie estaba interesado.
Doval
@Doval: bueno, OOP podría haber sido necesario para Java, pero no fue para C :) Pero es cierto que Java apuntaba a ser simple. Desafortunadamente, la gente parece asumir que un lenguaje simple lleva a programas simples, lo cual es un poco extraño (Brainfuck es un lenguaje muy simple ...), pero ciertamente estamos de acuerdo en que los lenguajes complicados (C ++ ...) tampoco son una panacea, aunque Pueden ser increíblemente útiles.
Matthieu M.
1
@MatthieuM .: Los sistemas reales son complejos. Un lenguaje bien diseñado cuyas complejidades coincidan con el sistema del mundo real que se está modelando puede permitir que el sistema complejo se modele con un código simple. Los intentos de simplificar en exceso un lenguaje simplemente empujan la complejidad al programador que lo está usando.
supercat
@supercat: no podría estar más de acuerdo. O como se parafrasea a Einstein: "Haz todo lo más simple posible, pero no más simple".
Matthieu M.
@MatthieuM .: Einstein fue sabio en muchos sentidos. Los lenguajes que intentan asumir que "todo es un objeto, una referencia a la cual se puede almacenar Object" no reconocen que las aplicaciones prácticas necesitan objetos mutables no compartidos y objetos inmutables compartibles (los cuales deben comportarse como valores), así como compartibles e inquebrantables. entidades. Usar un solo Objecttipo para todo no elimina la necesidad de tales distinciones; simplemente hace que sea más difícil usarlos correctamente.
supercat
5

Nulo / nulo / ninguno en sí mismo no es malo.

Si observa su famoso discurso engañosamente llamado "El error del billón de dólares", Tony Hoare habla sobre cómo permitir que cualquier variable pueda ser nula fue un gran error. La alternativa, usar Opciones, de hecho no elimina las referencias nulas. En su lugar, le permite especificar qué variables pueden mantenerse nulas y cuáles no.

De hecho, con los lenguajes modernos que implementan el manejo adecuado de excepciones, los errores de anulación de referencia no son diferentes a cualquier otra excepción: lo encuentra, lo arregla. Algunas alternativas a las referencias nulas (el patrón de objetos nulos, por ejemplo) ocultan errores, lo que hace que las cosas fallen silenciosamente hasta mucho más tarde. En mi opinión, es mucho mejor fallar rápido .

Entonces la pregunta es, ¿por qué los idiomas no implementan las Opciones? De hecho, el lenguaje posiblemente más popular de todos los tiempos C ++ tiene la capacidad de definir variables de objeto que no se pueden asignar NULL. Esta es una solución al "problema nulo" que Tony Hoare mencionó en su discurso. ¿Por qué el siguiente lenguaje mecanografiado más popular, Java, no lo tiene? Uno podría preguntarse por qué tiene tantos defectos en general, especialmente en su sistema de tipos. No creo que se pueda decir realmente que los idiomas cometen sistemáticamente este error. Algunos lo hacen, otros no.

BT
fuente
1
Una de las mayores fortalezas de Java desde una perspectiva de implementación, pero las debilidades desde la perspectiva del lenguaje, es que solo hay un tipo no primitivo: la Referencia de objetos promiscuos. Esto simplifica enormemente el tiempo de ejecución, haciendo posible algunas implementaciones de JVM extremadamente livianas. Sin embargo, ese diseño significa que cada tipo debe tener un valor predeterminado, y para una Referencia de objeto promiscuo, el único valor predeterminado posible es null.
supercat
Bueno, un tipo raíz no primitivo en cualquier caso. ¿Por qué es esto una debilidad desde la perspectiva del lenguaje? No entiendo por qué este hecho requiere que cada tipo tenga un valor predeterminado (o, por el contrario, por qué múltiples tipos de raíz permitirían que los tipos no tengan un valor predeterminado), ni por qué eso es una debilidad.
BT
¿Qué otro tipo de no primitivo podría contener un campo o elemento de matriz? La debilidad es que algunas referencias se utilizan para encapsular la identidad, y algunas para encapsular los valores contenidos dentro de los objetos identificados de ese modo. Para las variables de tipo de referencia utilizadas para encapsular identidad, nulles el único valor predeterminado razonable. Sin embargo, las referencias utilizadas para encapsular el valor podrían tener un comportamiento predeterminado sensible en los casos en que un tipo tendría o podría construir una instancia predeterminada sensible. Muchos aspectos de cómo deberían comportarse las referencias dependen de si encapsulan el valor y cómo lo hacen, pero ...
supercat
... el sistema de tipos Java no tiene forma de expresar eso. Si foocontiene la única referencia a un int[]contenedor {1,2,3}y el código desea foocontener una referencia a un int[]contenedor {2,2,3}, la forma más rápida de lograrlo sería incrementarlo foo[0]. Si el código quiere que un método sepa que se foocumple {1,2,3}, el otro método no modificará la matriz ni persistirá una referencia más allá del punto en el fooque desearía modificarla, la forma más rápida de lograrlo sería pasar una referencia a la matriz. Si Java tenía un tipo de "referencia efímera de solo lectura", entonces ...
supercat
... la matriz se podría pasar de forma segura como una referencia efímera, y un método que quisiera mantener su valor sabría que necesitaba copiarla. En ausencia de tal tipo, las únicas formas de exponer de manera segura el contenido de una matriz son hacer una copia de ella o encapsularla en un objeto creado solo para ese propósito.
supercat
4

Debido a que los lenguajes de programación generalmente están diseñados para ser prácticamente útiles en lugar de ser técnicamente correctos. El hecho es que los nullestados son una ocurrencia común debido a datos incorrectos o faltantes o un estado que aún no se ha decidido. Las soluciones técnicamente superiores son más difíciles de manejar que simplemente permitir estados nulos y absorber el hecho de que los programadores cometen errores.

Por ejemplo, si quiero escribir un script simple que funcione con un archivo, puedo escribir pseudocódigo como:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

y simplemente fallará si joebloggs.txt no existe. La cuestión es que, para los scripts simples, probablemente esté bien y para muchas situaciones en códigos más complejos, sé que existe y que la falla no sucederá, por lo que obligarme a verificar desperdicia mi tiempo. Las alternativas más seguras logran su seguridad al obligarme a lidiar correctamente con el posible estado de falla, pero a menudo no quiero hacer eso, solo quiero seguir adelante.

Jack Aidley
fuente
13
Y aquí dio un ejemplo de lo que está exactamente mal con los nulos. La función "openfile" implementada correctamente debería generar una excepción (para el archivo faltante) que detendría la ejecución allí mismo con una explicación exacta de lo que sucedió. En cambio, si devuelve nulo, se propaga aún más (a for line in file) y genera una excepción de referencia nula sin sentido, lo cual está bien para un programa tan simple pero causa problemas de depuración reales en sistemas mucho más complejos. Si no existieran los valores nulos, el diseñador de "archivo abierto" no podría cometer este error.
mrpyo
2
+1 para "Porque los lenguajes de programación generalmente están diseñados para ser prácticamente útiles en lugar de técnicamente correctos"
Martin Ba
2
Cada tipo de opción que conozco le permite hacer la falla en nulo con una única llamada de método adicional breve (ejemplo de Rust:) let file = something(...).unwrap(). Dependiendo de su POV, es una manera fácil de no manejar errores o una afirmación sucinta de que no puede ocurrir nulo. El tiempo perdido es mínimo, y ahorras tiempo en otros lugares porque no tienes que averiguar si algo puede ser nulo. Otra ventaja (que puede valer la pena por sí sola) es que ignora explícitamente el caso de error; cuando falla, hay pocas dudas de qué salió mal y dónde debe ir la solución.
44
@mrpyo No todos los idiomas admiten excepciones y / o manejo de excepciones (a la prueba / captura). Y también se puede abusar de las excepciones: "excepción como control de flujo" es un antipatrón común. Este escenario, un archivo no existe, es AFAIK el ejemplo más citado de ese antipatrón. Parece que estás reemplazando una mala práctica con otra.
David
8
@mrpyo if file exists { open file }sufre de una condición de carrera. La única forma confiable de saber si abrir un archivo tendrá éxito es intentar abrirlo.
4

Hay usos claros y prácticos del puntero NULL(o nil, o Nil, o null, o Nothingcomo se llame en su idioma preferido).

Para aquellos lenguajes que no tienen un sistema de excepción (por ejemplo, C), se puede usar un puntero nulo como marca de error cuando se debe devolver un puntero. Por ejemplo:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Aquí un NULLdevuelto de malloc(3)se utiliza como un marcador de falla.

Cuando se usa en argumentos de método / función, puede indicar el uso predeterminado para el argumento o ignorar el argumento de salida. Ejemplo a continuación.

Incluso para aquellos lenguajes con mecanismo de excepción, se puede usar un puntero nulo como indicación de error suave (es decir, errores recuperables) especialmente cuando el manejo de excepciones es costoso (por ejemplo, Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

Aquí, el error de software no hace que el programa se bloquee si no se detecta. Esto elimina el loco try-catch como Java y tiene un mejor control en el flujo del programa ya que los errores suaves no se interrumpen (y las pocas excepciones duras restantes generalmente no son recuperables y no se detectan)

Maxthon Chan
fuente
55
El problema es que no hay forma de distinguir las variables que nunca deberían contener nullde las que deberían. Por ejemplo, si quiero un nuevo tipo que contenga 5 valores en Java, podría usar una enumeración, pero lo que obtengo es un tipo que puede contener 6 valores (los 5 que quería + null). Es una falla en el sistema de tipos.
Doval
@Doval Si esa es la situación, simplemente asigne un significado a NULL (o si tiene un valor predeterminado, trátelo como un sinónimo del valor predeterminado) o use el NULL (que nunca debería aparecer en primer lugar) como un marcador de error suave (es decir, error pero al menos todavía no se bloquea)
Maxthon Chan
1
A @MaxtonChan Nullsolo se le puede asignar un significado cuando los valores de un tipo no contienen datos (por ejemplo, valores de enumeración). Tan pronto como sus valores sean algo más complicados (por ejemplo, una estructura), nullno se le puede asignar un significado que tenga sentido para ese tipo. No hay forma de usar a nullcomo estructura o lista. Y, de nuevo, el problema con el uso nullcomo señal de error es que no podemos decir qué puede devolver nulo o aceptar nulo. Cualquier variable en su programa podría ser a nullmenos que sea extremadamente meticuloso verificar cada una nullantes de cada uso, lo que nadie hace.
Doval
1
@Doval: No habría ninguna dificultad inherente particular en tener un tipo de referencia inmutable considerado nullcomo un valor predeterminado utilizable (por ejemplo, tener el valor predeterminado de stringcomportarse como una cadena vacía, de la forma en que lo hizo bajo el modelo de objeto común anterior). Todo lo que habría sido necesario habría sido que los idiomas los usaran en calllugar de callvirtinvocar miembros no virtuales.
supercat
@supercat Ese es un buen punto, pero ahora ¿no necesita agregar soporte para distinguir entre tipos inmutables y no inmutables? No estoy seguro de lo trivial que es agregar a un idioma.
Doval
4

Hay dos problemas relacionados, pero ligeramente diferentes:

  1. ¿Debería nullexistir en absoluto? ¿O debería usar siempre Maybe<T>donde nulo es útil?
  2. ¿Deberían todas las referencias ser anulables? Si no, ¿cuál debería ser el predeterminado?

    Tener que declarar explícitamente los tipos de referencia anulables como string?o similares evitaría la mayoría (pero no todos) de las nullcausas del problema , sin ser demasiado diferente de lo que los programadores están acostumbrados.

Al menos estoy de acuerdo con usted en que no todas las referencias deben ser anulables. Pero evitar nulo no está exento de complejidades:

.NET inicializa todos los campos default<T>antes de que el código administrado pueda acceder a ellos por primera vez. Esto significa que para los tipos de referencia que necesita nullo algo equivalente y que los tipos de valor se pueden inicializar a algún tipo de cero sin ejecutar el código. Si bien ambos tienen graves desventajas, la simplicidad de la defaultinicialización puede haber superado esas desventajas.

  • Por ejemplo, los campos que puede solucionar esto requieren la inicialización de los campos antes de exponer el thispuntero al código administrado. Spec # siguió esta ruta, utilizando una sintaxis diferente del encadenamiento del constructor en comparación con C #.

  • Para los campos estáticos, garantizar que esto sea más difícil, a menos que imponga fuertes restricciones sobre qué tipo de código puede ejecutarse en un inicializador de campo, ya que no puede simplemente ocultar el thispuntero.

  • ¿Cómo inicializar matrices de tipos de referencia? Considere uno List<T>que esté respaldado por una matriz con una capacidad mayor que la longitud. Los elementos restantes deben tener algún valor.

Otro problema es que no permite métodos como los bool TryGetValue<T>(key, out T value)que regresan default(T)como valuesi no encontraran nada. Aunque en este caso es fácil argumentar que el parámetro out es un mal diseño en primer lugar y este método debería devolver una unión discriminatoria o quizás un cambio.

Todos estos problemas pueden resolverse, pero no es tan fácil como "prohibir nulo y todo está bien".

CodesInChaos
fuente
En List<T>mi humilde opinión, es el mejor ejemplo, ya que requeriría que cada uno Ttenga un valor predeterminado, que cada elemento en la tienda de respaldo sea un Maybe<T>con un campo adicional "isValid", incluso cuando Tsea ​​un Maybe<U>, o que el código para el List<T>comportamiento sea diferente dependiendo sobre si Tes en sí mismo un tipo anulable. Consideraría que la inicialización de los T[]elementos a un valor predeterminado es la menos mala de esas elecciones, pero, por supuesto, significa que los elementos deben tener un valor predeterminado.
supercat
El óxido sigue al punto 1 - no es nulo en absoluto. Ceilán sigue el punto 2 - no nulo por defecto. Las referencias que pueden ser nulas se declaran explícitamente con un tipo de unión que incluye una referencia o nula, pero nula nunca puede ser el valor de una referencia simple. Como resultado, el lenguaje es completamente seguro y no hay NullPointerException porque no es semánticamente posible.
Jim Balter
2

Los lenguajes de programación más útiles permiten que los elementos de datos se escriban y lean en secuencias arbitrarias, de modo que a menudo no será posible determinar estáticamente el orden en que se realizarán las lecturas y escrituras antes de ejecutar un programa. Hay muchos casos en los que el código de hecho almacenará datos útiles en cada ranura antes de leerlo, pero donde probar eso sería difícil. Por lo tanto, a menudo será necesario ejecutar programas donde sea al menos teóricamente posible que el código intente leer algo que aún no se ha escrito con un valor útil. Ya sea que sea legal o no que el código lo haga, no hay una forma general de evitar que el código haga el intento. La única pregunta es qué debería ocurrir cuando eso ocurra.

Diferentes lenguajes y sistemas toman diferentes enfoques.

  • Un enfoque sería decir que cualquier intento de leer algo que no se ha escrito provocará un error inmediato.

  • Un segundo enfoque es requerir que el código proporcione algún valor en cada ubicación antes de que sea posible leerlo, incluso si no hubiera forma de que el valor almacenado sea semánticamente útil.

  • Un tercer enfoque es simplemente ignorar el problema y dejar que pase lo que suceda "naturalmente".

  • Un cuarto enfoque es decir que cada tipo debe tener un valor predeterminado, y cualquier ranura que no se haya escrito con otra cosa, tendrá ese valor predeterminado.

El enfoque n. ° 4 es mucho más seguro que el enfoque n. ° 3 y, en general, es más barato que los enfoques n. ° 1 y n. ° 2. Eso deja la pregunta de cuál debería ser el valor predeterminado para un tipo de referencia. Para los tipos de referencia inmutables, en muchos casos tendría sentido definir una instancia predeterminada y decir que el valor predeterminado para cualquier variable de ese tipo debería ser una referencia a esa instancia. Para los tipos de referencia mutables, sin embargo, eso no sería muy útil. Si se intenta utilizar un tipo de referencia mutable antes de que se haya escrito, generalmente no hay ningún curso de acción seguro, excepto atrapar en el punto de intento de uso.

Hablando semánticamente, si uno tiene una variedad customersde tipos Customer[20], y uno intenta Customer[4].GiveMoney(23)sin haber almacenado nada Customer[4], la ejecución tendrá que quedar atrapada. Se podría argumentar que un intento de lectura Customer[4]debería quedar atrapado de inmediato, en lugar de esperar hasta que el código intente hacerlo GiveMoney, pero hay suficientes casos en los que es útil leer un espacio, descubrir que no tiene un valor y luego usarlo. información, que tener el intento de lectura en sí mismo falla a menudo sería una gran molestia.

Algunos lenguajes permiten especificar que ciertas variables nunca deben contener nulo, y cualquier intento de almacenar un nulo debería desencadenar una trampa inmediata. Esa es una característica útil. Sin embargo, en general, cualquier lenguaje que permita a los programadores crear matrices de referencias tendrá que permitir la posibilidad de elementos de matriz nulos o forzar la inicialización de elementos de matriz a datos que posiblemente no puedan ser significativos.

Super gato
fuente
No sería un Maybe/ OptionTipo de resolver el problema con # 2, ya que si usted no tiene un valor para su referencia todavía , pero tendrá una en el futuro, sólo puede almacenar Nothingen una Maybe <Ref type>?
Doval
@Doval: No, no resolvería el problema, al menos, no sin introducir referencias nulas de nuevo. ¿Debería una "nada" actuar como un miembro del tipo? ¿Si es así, Cuál? ¿O debería arrojar una excepción? En cuyo caso, ¿cómo estás mejor que simplemente usarlo de manera nullcorrecta / sensata?
cHao
@Doval: ¿Debería el tipo de respaldo de a List<T>ser a T[]o a Maybe<T>? ¿Qué pasa con el tipo de respaldo de a List<Maybe<T>>?
supercat
@supercat No estoy seguro de cómo Maybetiene sentido un tipo de respaldo Listya que Maybetiene un solo valor. Quiso decir Maybe<T>[]?
Doval
@cHao Nothingsolo se puede asignar a valores de tipo Maybe, por lo que no es como asignar null. Maybe<T>y Tson dos tipos distintos.
Doval