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

78

Estoy seguro de que los diseñadores de lenguajes como Java o C # conocían problemas relacionados con la existencia de referencias nulas (consulte ¿Las referencias nulas realmente son algo malo? ). La implementación de un tipo de opción tampoco es mucho más compleja que las referencias nulas.

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

¿Es simplemente por el conservadurismo: "otros idiomas lo tienen, también debemos tenerlo ..."?

    
pregunta mrpyo 02.05.2014 - 14:19

10 respuestas

92

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

De 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 completo para referencias en un lenguaje orientado a objetos (ALGOL W). Mi objetivo era garantizar que todo uso de referencias fuera absolutamente seguro, con la verificación realizada automáticamente por el compilador. Pero no pude resistir la tentación de poner una referencia nula, simplemente porque fue muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallos en el sistema, que probablemente han causado mil millones. Dólares de dolor y daño en los últimos cuarenta años.

Énfasis 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 aún no entiendan por qué. mal. También es probable que sea en parte porque es conveniente que los nuevos idiomas sean similares a los antiguos, tanto por razones de marketing como de aprendizaje. Caso en cuestión:

  

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

(Fuente: enlace )

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

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

  

En general, los lenguajes de programación son mucho más complicados de lo que solían ser: la orientación de objetos, la herencia y otras características aún no se están considerando realmente desde el punto de vista de una base coherente y científicamente bien fundamentada. disciplina o una teoría de la corrección. Mi postulado original, que he estado persiguiendo como científico toda mi vida, es que uno utiliza los criterios de corrección como un medio de convergencia en un diseño de lenguaje de programación decente, uno que no No establezca trampas para sus usuarios, y aquellas en las que los diferentes componentes del programa se correspondan claramente con los diferentes componentes de su especificación, por lo que puede razonar sobre la composición. [...] Las herramientas, incluido el compilador, deben basarse en alguna teoría de lo que significa escribir un programa correcto.    - Entrevista oral de historia por Philip L. Frana, 17 de julio de 2002, Cambridge, Inglaterra; Charles Babbage Institute, Universidad de Minnesota. [ enlace

De nuevo, énfasis mío. Sun / Oracle y Microsoft son compañías, y el resultado final de cualquier compañía es el dinero. Los beneficios para ellos de tener null pueden haber superado a los contras, o pueden simplemente haber tenido una fecha límite demasiado ajustada para considerar completamente el problema. Como ejemplo de un error de lenguaje diferente que probablemente se produjo debido a fechas límite:

  

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 cierre del mercado. El equipo de Java original hizo un trabajo increíble, pero no todas las API son perfectas. Cloneable es un punto débil, y creo que la gente debería ser consciente de sus limitaciones. -Josh Bloch

(Fuente: enlace )

    
respondido por el Doval 02.05.2014 - 14:38
119
  

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.

  

La implementación de un tipo de opción tampoco es mucho más compleja que las referencias nulas.

¡No estoy de acuerdo! Las consideraciones de diseño que se incluyeron en los tipos de valor anulable en C # 2 fueron complejas, controvertidas y difíciles. Tomaron los equipos de diseño de los lenguajes y el tiempo de ejecución durante muchos meses de debate, implementación de prototipos, etc., y de hecho, la semántica del boxeo sin anulamiento se cambió muy cerca del envío de C # 2.0, que fue muy controvertido. p>

  

¿Por qué decidieron incluirlo de todos modos?

Todo diseño es un proceso de elegir entre muchos objetivos sutilmente 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. No existen tipos de referencia no anulables, 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 sencilla con COM es importante.

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

  • La interoperabilidad sencilla con las bases de datos es importante.

  • La consistencia de la semántica es importante; si tenemos una referencia a TheKingOfFrance igual a null, 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 noción de tener un rey en Francia es absurda, 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.

  • Es importante ser susceptible al análisis estático.

  • La consistencia del sistema de tipos es importante; ¿podemos siempre saber que una referencia no anulable es nunca en alguna circunstancia observada como inválida? ¿Qué sucede en el constructor de un objeto con un campo de tipo de referencia no anulable? ¿Qué sucede en el finalizador de un objeto de este tipo, donde el objeto se finaliza porque el código que se suponía que debía completar en la referencia arrojó una excepción ? Un sistema tipográfico que te miente sobre sus garantías es peligroso.

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

  • ¿Podemos implementar la función sin romper otras funciones? ¿Qué otras posibles funciones futuras excluye la función?

  • 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 de Maybe<T> como alternativa es un no iniciador completo. ¿Debería .NET haberse deslizado durante dos años mientras el equipo de tiempo de ejecución agregó genéricos, únicamente para eliminar las referencias nulas?

  • ¿Qué pasa con la consistencia del sistema de tipos? Puedes decir Nullable<T> para cualquier tipo de valor: no, espera, eso es una mentira. No puedes decir Nullable<Nullable<T>> . ¿Deberías poder? Si es así, ¿cuáles son sus semánticas deseadas? ¿Vale la pena hacer que todo el sistema de tipos tenga un caso especial solo para esta función?

Y así sucesivamente. Estas decisiones son complejas.

    
respondido por el Eric Lippert 02.05.2014 - 23:13
27

Null sirve para 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 el sufrimiento que pueden causar, especialmente cuando se usan de manera liberal.

Mi postura personal es que la gente puede usar nulos solo cuando pueden justificar que es necesario y apropiado.

Ejemplo que justifica nulos:

La fecha de la muerte suele ser un campo que puede contener nulos. Hay tres situaciones posibles con fecha de fallecimiento. O la persona murió y se conoce la fecha, la persona falleció y la fecha se desconoce, o la persona no está muerta y, por lo tanto, no existe una fecha de muerte.

La fecha de la muerte 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 muriera en ese momento y se marque como su "valor vacío" si tuviera que usa la fecha por defecto.

Los datos deberían representar la situación correctamente.

La persona es muerta. Se conoce la fecha de la muerte (3/9/1984)

Simple, '3/9/1984'

La persona es muerta. Se desconoce la fecha de la muerte

Entonces, ¿qué es lo mejor? Nulo , '0/0/0000' o '01 / 01/1869 '(o cualquiera que sea su valor predeterminado?)

La persona no está muerta. La fecha de muerte no es aplicable

Entonces, ¿qué es lo mejor? Nulo , '0/0/0000' o '01 / 01/1869 '(o cualquiera que sea su valor predeterminado?)

Así que pensemos en cada valor sobre ...

  • Nulo , tiene implicaciones y preocupaciones de las que debe tener cuidado, por casualidad, tratar 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 adecuada de ninguna fecha. Desafortunadamente, algunos idiomas y la validación rechazarán esto como una fecha no válida, lo que hace que sea un no go en muchos casos.
  • 1/1/1869 (o cualquiera que sea su valor de fecha y hora predeterminado) , el problema aquí es que es difícil de manejar. Podría usar eso como su falta de valor, excepto lo que 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 murieron en esa fecha, lo que podría causar problemas de integridad de los datos.

El hecho es a veces que Do no representa nada y que a veces un tipo de variable funciona bien para eso, pero a menudo los tipos de variables no pueden representar nada.

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

Por todos los medios, el nulo es abusado y potencialmente peligroso, pero a veces es necesario. Es solo el valor predeterminado en muchos casos porque hasta que proporciono un valor, la falta de un valor y algo debe representarlo. (Nulo)

    
respondido por el RualStorge 02.05.2014 - 21:42
9

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 Joneses. Una característica clave de cualquier idioma nuevo es la capacidad de interoperar con bibliotecas existentes en otros idiomas (lea: C). Como C tiene punteros nulos, la capa de interoperabilidad necesita necesariamente el concepto de nulo (o algún otro equivalente "no existe" que explote cuando lo use).

El diseñador de idiomas podría haber elegido utilizar Tipos de opción y obligarte a manejar la ruta nula en todas partes que las cosas podrían ser nulas. Y eso casi seguramente daría lugar a menos errores.

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

    
respondido por el Telastyn 02.05.2014 - 14:33
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 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, ... todo uso un null similar.

¿Por qué? Bueno, ¿cuál es la alternativa?

En lenguajes funcionales como Haskell tienes el tipo Option o Maybe ; sin embargo, están construidos sobre:

  • tipos paramétricos
  • tipos de datos algebraicos

Ahora, ¿el C, Java, Python, Ruby o PHP original es compatible con alguna de estas características? No. Los genéricos defectuosos de Java son recientes en la historia del lenguaje y de alguna manera dudo que los otros incluso los implementen en absoluto.

Ahí lo tienes. null es fácil, los tipos de datos algebraicos paramétricos son más difíciles. La gente optó por la alternativa más simple.

    
respondido por el Matthieu M. 03.05.2014 - 17:19
4

Porque los lenguajes de programación generalmente están diseñados para ser prácticamente útiles en lugar de técnicamente correctos. El hecho es que los estados null son una ocurrencia común debido a datos erróneos o faltantes o un estado que aún no se ha decidido. Las soluciones técnicamente superiores son todas 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, eso es probablemente correcto y para muchas situaciones en códigos más complejos, sé que existe y que la falla no ocurrirá, por lo que obligarme a verificar la pérdida de tiempo. Las alternativas más seguras logran su seguridad al obligarme a lidiar correctamente con el estado de falla potencial, pero a menudo no quiero hacer eso, solo quiero seguir adelante.

    
respondido por el Jack Aidley 02.05.2014 - 19:27
4

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

Para aquellos idiomas que no tienen un sistema de excepción (por ejemplo, C), un puntero nulo se puede usar 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í se usa un NULL devuelto desde malloc(3) como marcador de falla.

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

Incluso para aquellos idiomas con mecanismo de excepción, se puede usar un puntero nulo como indicación de error suave (es decir, errores que se pueden recuperar) 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 de software no interrumpen (y las pocas excepciones duras que quedan no son recuperables y quedan sin ser detectadas)

    
respondido por el Maxthon Chan 02.05.2014 - 19:43
4

Hay dos temas relacionados, pero ligeramente diferentes:

  1. ¿Debería null existir? ¿O debería usar siempre Maybe<T> donde null es útil?
  2. ¿Todas las referencias deben ser anulables? Si no, ¿cuál debería ser el predeterminado?

    Tener que declarar explícitamente los tipos de referencia anulables como string? o similar evitaría la mayoría (pero no todos) de los problemas que null causa, sin ser muy 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 el nulo no está exento de complejidades:

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

  • Para campos de instancia puede solucionar esto al requerir la inicialización de los campos antes de exponer el puntero this al código administrado. Spec # siguió esta ruta, usando una sintaxis diferente del encadenamiento de constructores en comparación con C #.

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

  • ¿Cómo inicializar matrices de tipos de referencia? Considere un 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 bool TryGetValue<T>(key, out T value) que devuelven default(T) como value si no encuentran nada. Aunque en este caso es fácil argumentar que el parámetro de salida es un mal diseño en primer lugar y este método debería devolver una unión discriminatoria o un quizás en su lugar.

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

    
respondido por el CodesInChaos 02.05.2014 - 20:39
4

Null / nil / none en sí mismo no es malo.

Si ve a su famoso y engañoso nombre "El error de los mil millones de dólares", Tony Hoare habla de cómo permitir que cualquier variable cualquier sea capaz de mantener nula fue un gran error. La alternativa, usar Opciones, no , de hecho, 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 las excepciones, los errores de anulación de nulos no son diferentes a cualquier otra excepción: usted lo encuentra, lo arregla. Algunas alternativas a las referencias nulas (el patrón de Objeto nulo, por ejemplo) ocultan errores, lo que hace que las cosas falle silenciosamente hasta mucho más tarde. En mi opinión, es mucho mejor fallar rápidamente .

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

    
respondido por el B T 04.05.2014 - 05:30
2

La mayoría de los lenguajes de programación útiles permiten escribir y leer elementos de datos en secuencias arbitrarias, de modo que a menudo no será posible determinar de forma estática el orden en que se producirán las lecturas y escrituras antes de ejecutar un programa. Hay muchos casos en los que el código almacenará datos útiles en cada ranura antes de leerlos, pero demostrarlo sería difícil. Por lo tanto, a menudo será necesario ejecutar programas en los que, al menos en teoría, sea 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 impedir que el código intente. La única pregunta es qué debería ocurrir cuando eso ocurre.

Diferentes idiomas y sistemas tienen diferentes enfoques.

  • Un enfoque sería decir que cualquier intento de leer algo que no se haya escrito generará 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 ocurra lo que suceda "naturalmente", simplemente suceda.

  • Un cuarto enfoque es decir que cada tipo debe tener un valor predeterminado, y cualquier ranura que no se haya escrito con nada más se establecerá de manera predeterminada en ese valor.

El enfoque # 4 es mucho más seguro que el enfoque # 3, y en general es más barato que los enfoques # 1 y # 2. Eso deja la pregunta de cuál debería ser el valor predeterminado para un tipo de referencia. Para 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. Sin embargo, para los tipos de referencia mutables, eso no sería muy útil. Si se intenta utilizar un tipo de referencia mutable antes de que se haya escrito, generalmente no hay un curso de acción seguro, excepto para atraparlo en el punto de intento de uso.

Hablando semánticamente, si uno tiene un array customers de tipo Customer[20] , y uno intenta Customer[4].GiveMoney(23) sin haber almacenado nada en Customer[4] , la ejecución tendrá que interceptarse. Se podría argumentar que un intento de leer Customer[4] debería interceptarse inmediatamente, en lugar de esperar hasta que el código intente GiveMoney , pero hay suficientes casos en los que es útil leer un espacio, descubrir que no tiene un valor, y luego hacer uso de esa información, el hecho de que el intento de lectura fallara a menudo sería una gran molestia.

Algunos idiomas permiten que se especifique que ciertas variables nunca deben contener nulos, y cualquier intento de almacenar un nulo debería desencadenar una captura 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 bien forzar la inicialización de elementos de matriz a datos que no pueden ser significativos.

    
respondido por el supercat 03.05.2014 - 05:22

Lea otras preguntas en las etiquetas

Comentarios Recientes

<| endoftext |> ¡Imagínese! Películas, televisores, computadoras, arqueros están construidos en LegoStory por Randall Carson - Una tarde de octubre este fin de semana: Kid llegó a la guardería esperando encontrar a su madre (Noah Haas) no muy lejos de la famosa comuna de Beowolf. Llegó esperando sordidez, que detestaba. Lo que descubrió ese día fue uno de los mejores recuerdos de la historia, no lejos de Denver, Colorado, es una aldea de jóvenes adultos adoptivos, reunidos por los fieles de Beowolf para luchar... Lee mas