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 ..."?
language-design
null
mrpyo
fuente
fuente
Respuestas:
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 :
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:
(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:
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
null
pueden 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:(Fuente: http://www.artima.com/intv/bloch13.html )
fuente
null
en 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 quierennull
, se los dannull
. También supones que han aceptadonull
que les está costando dinero.ICloneable
está igualmente roto en .NET; Desafortunadamente, este es un lugar donde las deficiencias de Java no se aprendieron a tiempo.Por supuesto.
¡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.
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 decirNullable<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.
fuente
catches
eso 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.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 ...
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)
fuente
Null serves a very valid purpose of representing a lack of value.
Un tipoOption
oMaybe
sirve para este propósito muy válido sin pasar por alto el sistema de tipos.NOT NULL
. Sin embargo, mi pregunta no estaba relacionada con RDBMS ...Person
tipo tiene unstate
campo de tipoState
, que es una unión discriminada deAlive
yDead(dateOfDeath : Date)
.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. ..
fuente
Option
para usarlo en Scala, que es tan fácil comoval x = Option(possiblyNullReference)
. En la práctica, las personas no tardan mucho en ver los beneficios de unOption
.Match
método que tome delegados como argumentos. Luego pasa expresiones lambda aMatch
(puntos de bonificación por usar argumentos con nombre) yMatch
llama a la correcta.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
null
referencias (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 similarnull
.Por qué ? Bueno, cual es la alternativa?
En lenguajes funcionales como Haskell tiene el tipo
Option
oMaybe
; Sin embargo, estos se basan en: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.
null
Es fácil, los tipos de datos algebraicos paramétricos son más difíciles. La gente fue por la alternativa más simple.fuente
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 soloObject
tipo para todo no elimina la necesidad de tales distinciones; simplemente hace que sea más difícil usarlos correctamente.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.fuente
null
.null
es 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 ...foo
contiene la única referencia a unint[]
contenedor{1,2,3}
y el código deseafoo
contener una referencia a unint[]
contenedor{2,2,3}
, la forma más rápida de lograrlo sería incrementarlofoo[0]
. Si el código quiere que un método sepa que sefoo
cumple{1,2,3}
, el otro método no modificará la matriz ni persistirá una referencia más allá del punto en elfoo
que 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 ...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
null
estados 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:
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.
fuente
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.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.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.Hay usos claros y prácticos del puntero
NULL
(onil
, oNil
, onull
, oNothing
como 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:
Aquí un
NULL
devuelto demalloc(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):
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)
fuente
null
de 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.Null
solo 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),null
no se le puede asignar un significado que tenga sentido para ese tipo. No hay forma de usar anull
como estructura o lista. Y, de nuevo, el problema con el usonull
como señal de error es que no podemos decir qué puede devolver nulo o aceptar nulo. Cualquier variable en su programa podría ser anull
menos que sea extremadamente meticuloso verificar cada unanull
antes de cada uso, lo que nadie hace.null
como un valor predeterminado utilizable (por ejemplo, tener el valor predeterminado destring
comportarse 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 encall
lugar decallvirt
invocar miembros no virtuales.Hay dos problemas relacionados, pero ligeramente diferentes:
null
existir en absoluto? ¿O debería usar siempreMaybe<T>
donde nulo es útil?¿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 lasnull
causas 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 necesitanull
o 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 ladefault
inicializació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
this
puntero 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
this
puntero.¿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 regresandefault(T)
comovalue
si 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".
fuente
List<T>
mi humilde opinión, es el mejor ejemplo, ya que requeriría que cada unoT
tenga un valor predeterminado, que cada elemento en la tienda de respaldo sea unMaybe<T>
con un campo adicional "isValid", incluso cuandoT
sea unMaybe<U>
, o que el código para elList<T>
comportamiento sea diferente dependiendo sobre siT
es en sí mismo un tipo anulable. Consideraría que la inicialización de losT[]
elementos a un valor predeterminado es la menos mala de esas elecciones, pero, por supuesto, significa que los elementos deben tener un valor predeterminado.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
customers
de tiposCustomer[20]
, y uno intentaCustomer[4].GiveMoney(23)
sin haber almacenado nadaCustomer[4]
, la ejecución tendrá que quedar atrapada. Se podría argumentar que un intento de lecturaCustomer[4]
debería quedar atrapado de inmediato, en lugar de esperar hasta que el código intente hacerloGiveMoney
, 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.
fuente
Maybe
/Option
Tipo 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 almacenarNothing
en unaMaybe <Ref type>
?null
correcta / sensata?List<T>
ser aT[]
o aMaybe<T>
? ¿Qué pasa con el tipo de respaldo de aList<Maybe<T>>
?Maybe
tiene sentido un tipo de respaldoList
ya queMaybe
tiene un solo valor. Quiso decirMaybe<T>[]
?Nothing
solo se puede asignar a valores de tipoMaybe
, por lo que no es como asignarnull
.Maybe<T>
yT
son dos tipos distintos.