used top the programming programing paid new most learn languages language for development best programming-languages functional-programming null nullpointerexception non-nullable

programming-languages - top - the best programming language



Mejor explicación para lenguajes sin nulo. (11)

Asamblea nos trajo direcciones también conocidas como punteros sin tipo. C los asignó directamente como punteros escritos, pero introdujo el nulo de Algol como un valor de puntero único, compatible con todos los punteros escritos. El gran problema con el nulo en C es que dado que cada puntero puede ser nulo, uno nunca puede usar un puntero de forma segura sin una comprobación manual.

En lenguajes de nivel superior, tener nulo es incómodo ya que realmente transmite dos nociones distintas:

  • Decir que algo no está definido .
  • Decir que algo es opcional .

Tener variables no definidas es bastante inútil, y produce un comportamiento indefinido cuando ocurren. Supongo que todos estarán de acuerdo en que tener las cosas sin definir debe evitarse a toda costa.

El segundo caso es opcional y se proporciona mejor explícitamente, por ejemplo, con un tipo de opción .

Digamos que estamos en una empresa de transporte y necesitamos crear una aplicación para ayudar a crear un horario para nuestros conductores. Para cada conductor, almacenamos algunas informaciones tales como: las licencias de conducir que tienen y el número de teléfono para llamar en caso de emergencia.

En C podríamos tener:

struct PhoneNumber { ... }; struct MotorbikeLicence { ... }; struct CarLicence { ... }; struct TruckLicence { ... }; struct Driver { char name[32]; /* Null terminated */ struct PhoneNumber * emergency_phone_number; struct MotorbikeLicence * motorbike_licence; struct CarLicence * car_licence; struct TruckLicence * truck_licence; };

Como puede observar, en cualquier procesamiento sobre nuestra lista de controladores tendremos que verificar los punteros nulos. El compilador no te ayudará, la seguridad del programa depende de tus hombros.

En OCaml, el mismo código se vería así:

type phone_number = { ... } type motorbike_licence = { ... } type car_licence = { ... } type truck_licence = { ... } type driver = { name: string; emergency_phone_number: phone_number option; motorbike_licence: motorbike_licence option; car_licence: car_licence option; truck_licence: truck_licence option; }

Digamos ahora que queremos imprimir los nombres de todos los conductores junto con los números de licencia de sus camiones.

Cía:

#include <stdio.h> void print_driver_with_truck_licence_number(struct Driver * driver) { /* Check may be redundant but better be safe than sorry */ if (driver != NULL) { printf("driver %s has ", driver->name); if (driver->truck_licence != NULL) { printf("truck licence %04d-%04d-%08d/n", driver->truck_licence->area_code driver->truck_licence->year driver->truck_licence->num_in_year); } else { printf("no truck licence/n"); } } } void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) { if (drivers != NULL && nb >= 0) { int i; for (i = 0; i < nb; ++i) { struct Driver * driver = drivers[i]; if (driver) { print_driver_with_truck_licence_number(driver); } else { /* Huh ? We got a null inside the array, meaning it probably got corrupt somehow, what do we do ? Ignore ? Assert ? */ } } } else { /* Caller provided us with erroneous input, what do we do ? Ignore ? Assert ? */ } }

En OCaml eso sería:

open Printf (* Here we are guaranteed to have a driver instance *) let print_driver_with_truck_licence_number driver = printf "driver %s has " driver.name; match driver.truck_licence with | None -> printf "no truck licence/n" | Some licence -> (* Here we are guaranteed to have a licence *) printf "truck licence %04d-%04d-%08d/n" licence.area_code licence.year licence.num_in_year (* Here we are guaranteed to have a valid list of drivers *) let print_drivers_with_truck_licence_numbers drivers = List.iter print_driver_with_truck_licence_number drivers

Como puede ver en este ejemplo trivial, no hay nada complicado en la versión segura:

  • Es terser.
  • Usted obtiene garantías mucho mejores y no se requiere ninguna comprobación nula en absoluto.
  • El compilador aseguró que usted trató correctamente con la opción

Mientras que en C, podrías haber olvidado un cheque nulo y un boom ...

Nota: estos ejemplos de código no fueron compilados, pero espero que tengan las ideas.

De vez en cuando, los programadores se quejan de nulos errores / excepciones, alguien pregunta qué hacemos sin nulo.

Tengo una idea básica de la frialdad de los tipos de opción, pero no tengo el conocimiento o la habilidad de idiomas para expresarlo mejor. ¿Cuál es una gran explicación de lo siguiente, escrita de manera que sea accesible para el programador promedio al que podemos dirigir a esa persona?

  • La inconveniencia de tener referencias / punteros puede ser anulable por defecto
  • Cómo funcionan los tipos de opciones, incluidas las estrategias para facilitar la comprobación de casos nulos como
    • coincidencia de patrones y
    • comprensiones monádicas
  • Solución alternativa como el mensaje comiendo nada.
  • (Otros aspectos que me perdí)

Creo que el resumen sucinto de por qué nulo no es deseable es que los estados sin sentido no deberían ser representables .

Supongamos que estoy modelando una puerta. Puede estar en uno de tres estados: abrir, cerrar pero desbloquear, y cerrar y bloquear. Ahora podría modelarlo siguiendo las líneas de

class Door private bool isShut private bool isLocked

y está claro cómo asignar mis tres estados en estas dos variables booleanas. Pero esto deja un cuarto estado no deseado disponible: isShut==false && isLocked==true . Debido a que los tipos que he seleccionado como mi representación admiten este estado, debo dedicar un esfuerzo mental para asegurar que la clase nunca llegue a este estado (quizás codificando explícitamente un invariante). En contraste, si estuviera usando un idioma con tipos de datos algebraicos o enumeraciones comprobadas que me permite definir

type DoorState = | Open | ShutAndUnlocked | ShutAndLocked

entonces podría definir

class Door private DoorState state

Y no hay más preocupaciones. El sistema de tipos garantizará que solo haya tres estados posibles para una instancia de la class Door . Esto es en lo que son buenos los sistemas de tipos: descartar explícitamente toda una clase de errores en el momento de la compilación.

El problema con el null es que cada tipo de referencia obtiene este estado adicional en su espacio que generalmente no es deseado. Una variable de string podría ser cualquier secuencia de caracteres, o podría ser este valor null extra loco que no se asigna a mi dominio problemático. Un objeto de Triangle tiene tres Point s, que a su vez tienen valores de X e Y , pero desafortunadamente, los Point o el Triangle sí pueden ser este valor nulo loco que no tiene sentido para el dominio de gráficos en el que estoy trabajando. Etc.

Cuando tiene la intención de modelar un valor posiblemente inexistente, debe optar por él explícitamente. Si la forma en que pretendo modelar a las personas es que cada Person tenga un FirstName y un LastName , pero solo algunas personas tienen el nombre de MiddleName , entonces me gustaría decir algo como

class Person private string FirstName private Option<string> MiddleName private string LastName

donde se supone que la string aquí es un tipo no anulable. Entonces no hay invariantes difíciles de establecer y no hay NullReferenceException inesperadas al tratar de calcular la longitud del nombre de alguien. El sistema de tipos garantiza que cualquier código que se ocupe de MiddleName cuenta la posibilidad de que sea None , mientras que cualquier código que se ocupe de FirstName puede asumir con seguridad que existe un valor allí.

Entonces, por ejemplo, usando el tipo anterior, podríamos crear esta función tonta:

let TotalNumCharsInPersonsName(p:Person) = let middleLen = match p.MiddleName with | None -> 0 | Some(s) -> s.Length p.FirstName.Length + middleLen + p.LastName.Length

sin preocupaciones En contraste, en un lenguaje con referencias anulables para tipos como cadenas, luego asumiendo

class Person private string FirstName private string MiddleName private string LastName

terminas creando cosas como

let TotalNumCharsInPersonsName(p:Person) = p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

que explota si el objeto Person entrante no tiene el valor invariable de que todo no sea nulo, o

let TotalNumCharsInPersonsName(p:Person) = (if p.FirstName=null then 0 else p.FirstName.Length) + (if p.MiddleName=null then 0 else p.MiddleName.Length) + (if p.LastName=null then 0 else p.LastName.Length)

o tal vez

let TotalNumCharsInPersonsName(p:Person) = p.FirstName.Length + (if p.MiddleName=null then 0 else p.MiddleName.Length) + p.LastName.Length

Suponiendo que p asegura que el primero / el último están allí, pero el medio puede ser nulo, o tal vez haga verificaciones que arrojen diferentes tipos de excepciones, o quién sabe qué. Todas estas locas opciones de implementación y cosas para pensar surgen porque hay un valor representable estúpido que no quieres o necesitas.

Nulo normalmente agrega complejidad innecesaria. La complejidad es el enemigo de todo software, y debe esforzarse por reducir la complejidad cuando sea razonable.

(Tenga en cuenta que existe una mayor complejidad incluso para estos ejemplos simples. Incluso si un FirstName no puede ser null , una string puede representar "" (la cadena vacía), que probablemente no sea el nombre de una persona que intentamos modelar. Como tal , incluso con cadenas no anulables, podría ser el caso de que estemos "representando valores sin sentido". Nuevamente, puede optar por combatir esto mediante invariantes y código condicional en tiempo de ejecución, o utilizando el sistema de tipos (por ejemplo, tener un tipo NonEmptyString ). Este último es quizás poco aconsejable (los tipos "buenos" a menudo se "cierran" sobre un conjunto de operaciones comunes, y por ejemplo NonEmptyString no se cierra sobre .SubString(0,0) ), pero demuestra más puntos en el espacio de diseño. Al final del día, en cualquier sistema de tipo dado, hay cierta complejidad de la que será muy bueno deshacerse de él, y otra complejidad que es intrínsecamente más difícil de eliminar. La clave de este tema es que en casi todos los sistemas de tipos, el cambio de "referencias anulables por defecto" a "referencias no anulables por defecto" es casi siempre un cambio simple que hace que el sistema de tipos sea mucho mejor para combatir la complejidad y descartar ciertos tipos de errores y estados sin sentido. Así que es una locura que tantos idiomas repitan este error una y otra vez.)


Dado que la gente parece faltarlo: null es ambiguo.

La fecha de nacimiento de Alice es null . Qué significa eso?

La fecha de la muerte de Bob es null . Qué significa eso?

Una interpretación "razonable" podría ser que la fecha de nacimiento de Alicia existe pero se desconoce, mientras que la fecha de muerte de Bob no existe (Bob sigue vivo). Pero, ¿por qué llegamos a diferentes respuestas?

Otro problema: null es un caso de borde.

  • Es null = null ?
  • Es nan = nan ?
  • Es inf = inf ?
  • ¿Es +0 = -0 ?
  • ¿Es +0/0 = -0/0 ?

Las respuestas suelen ser "sí", "no", "sí", "sí", "no", "sí" respectivamente. Los "matemáticos" locos llaman a NaN "nulidad" y dicen que se compara igual a sí mismo. SQL trata los nulos como si no fueran iguales a nada (por lo que se comportan como NaN). Uno se pregunta qué sucede cuando intenta almacenar ± ∞, ± 0 y NaNs en la misma columna de la base de datos (hay 2 53 NaNs, la mitad de los cuales son "negativos").

Para empeorar las cosas, las bases de datos difieren en la forma en que tratan a NULL, y la mayoría de ellas no son coherentes (consulte Manejo de NULL en SQLite para obtener una descripción general). Es bastante horrible

Y ahora para la historia obligatoria:

Recientemente diseñé una tabla de base de datos (sqlite3) con cinco columnas a NOT NULL, b, id_a, id_b NOT NULL, timestamp . Debido a que es un esquema genérico diseñado para resolver un problema genérico para aplicaciones bastante arbitrarias, existen dos restricciones de exclusividad:

UNIQUE(a, b, id_a) UNIQUE(a, b, id_b)

id_a solo existe para la compatibilidad con un diseño de aplicación existente (en parte porque no he encontrado una mejor solución), y no se usa en la nueva aplicación. Debido a la forma en que NULL funciona en SQL, puedo insertar (1, 2, NULL, 3, t) y (1, 2, NULL, 4, t) y no violar la primera restricción de unicidad (porque (1, 2, NULL) != (1, 2, NULL) ).

Esto funciona específicamente debido a cómo NULL funciona en una restricción de singularidad en la mayoría de las bases de datos (presumiblemente, por lo que es más fácil modelar situaciones del "mundo real", por ejemplo, no hay dos personas que puedan tener el mismo Número de Seguro Social, pero no todas las personas tienen una).

FWIW, sin invocar primero el comportamiento indefinido, las referencias de C ++ no pueden "apuntar a" nulo, y no es posible construir una clase con variables de miembro de referencia sin inicializar (si se produce una excepción, la construcción falla).

Nota: Ocasionalmente, es posible que desee punteros mutuamente exclusivos (es decir, solo uno de ellos puede ser no NULL), por ejemplo, en un type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed hipotético de iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed En su lugar, me veo obligado a hacer cosas como assert((bool)actionSheet + (bool)alertView == 1) .


Desde el fondo .NET, siempre pensé que null tenía un punto, es útil. Hasta que llegué a conocer las estructuras y lo fácil que era trabajar con ellas evitando un montón de código repetitivo. Tony Hoare hablando en QCon London en 2009, se disculpó por inventar la referencia nula . Para citarlo:

Yo 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 tipos 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 era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallas en el sistema, que probablemente hayan causado un dolor y un daño de mil millones de dólares en los últimos cuarenta años. En los últimos años, se han utilizado varios analizadores de programas como PREfix y PREfast en Microsoft para verificar las referencias y advertir si existe el riesgo de que no sean nulos. Los lenguajes de programación más recientes como Spec # han introducido declaraciones para referencias no nulas. Esta es la solución, que rechacé en 1965.

Vea esta pregunta también en programadores


Lo bueno de los tipos de opciones no es que sean opcionales. Es que todos los demás tipos no lo son .

A veces , necesitamos poder representar un tipo de estado "nulo". A veces tenemos que representar una opción "sin valor", así como los otros valores posibles que una variable puede tomar. Por lo tanto, un lenguaje que desapruebe completamente esto va a estar un poco paralizado.

Pero a menudo , no lo necesitamos, y permitir ese estado "nulo" solo conduce a la ambigüedad y la confusión: cada vez que accedo a una variable de tipo de referencia en .NET, debo considerar que puede ser nulo .

A menudo, en realidad nunca será nulo, porque el programador estructura el código para que nunca pueda suceder. Pero el compilador no puede verificar eso, y cada vez que lo veas, debes preguntarte "¿esto puede ser nulo? ¿Necesito comprobar si hay nulos aquí?"

Idealmente, en los muchos casos en que el nulo no tiene sentido, no debería permitirse .

Eso es difícil de lograr en .NET, donde casi todo puede ser nulo. Debe confiar en el autor del código al que está llamando para ser 100% disciplinado y coherente y haber documentado claramente lo que puede y no puede ser nulo, o tiene que estar paranoico y verificar todo .

Sin embargo, si los tipos no son anulables por defecto , entonces no es necesario verificar si son nulos o no. Sabes que nunca pueden ser nulos, porque el compilador / comprobador de tipo lo aplica por ti.

Y luego solo necesitamos una puerta trasera para los casos excepcionales en los que necesitamos manejar un estado nulo. Entonces se puede usar un tipo de "opción". Luego permitimos nulo en los casos en que tomamos una decisión consciente de que necesitamos poder representar el caso "sin valor" y, en cualquier otro caso, sabemos que el valor nunca será nulo.

Como han mencionado otros, en C # o Java, por ejemplo, null puede significar una de dos cosas:

  1. La variable no está inicializada. Esto debería, idealmente, nunca suceder. Una variable no debería existir a menos que esté inicializada.
  2. La variable contiene algunos datos "opcionales": debe poder representar el caso donde no hay datos . Esto es a veces necesario. Tal vez esté tratando de encontrar un objeto en una lista, y no sabe de antemano si está allí o no. Entonces necesitamos poder representar que "no se encontró ningún objeto".

El segundo significado debe conservarse, pero el primero debe eliminarse por completo. E incluso el segundo significado no debería ser el predeterminado. Es algo a lo que podemos optar si y cuando lo necesitamos . Pero cuando no necesitamos que algo sea opcional, queremos que el comprobador de tipos garantice que nunca será nulo.


Los lenguajes vectoriales a veces pueden salirse con la suya sin tener un nulo.

El vector vacío sirve como un nulo escrito en este caso.


Microsoft Research tiene un proyecto de intersting llamado

Especulación#

Es una extensión de C # con tipo no nulo y algún mecanismo para verificar que sus objetos no sean nulos , aunque, en mi humilde opinión, aplicar el principio de diseño por contrato puede ser más apropiado y más útil para muchas situaciones problemáticas causadas por referencias nulas.



Siempre he considerado a Null (o nil) como la ausencia de un valor .

A veces quieres esto, a veces no. Depende del dominio con el que estés trabajando. Si la ausencia es significativa: no hay segundo nombre, entonces su aplicación puede actuar en consecuencia. Por otro lado, si el valor nulo no debería estar allí: el primer nombre es nulo, entonces el desarrollador recibe la proverbial llamada de las 2 am.

También he visto un código sobrecargado y demasiado complicado con las comprobaciones de null. Para mí esto significa una de dos cosas:
a) un error más arriba en el árbol de la aplicación
b) diseño malo / incompleto

En el lado positivo: Null es probablemente una de las nociones más útiles para verificar si algo está ausente, y los idiomas sin el concepto de null terminarán complicando las cosas cuando sea el momento de validar los datos. En este caso, si no se inicializa una nueva variable, dichos lenguajes normalmente establecerán las variables en una cadena vacía, 0, o una colección vacía. Sin embargo, si una cadena vacía o 0 o una colección vacía son valores válidos para su aplicación, entonces tiene un problema.

En ocasiones, esto se evita mediante la invención de valores especiales / extraños para que los campos representen un estado sin inicializar. Pero entonces, ¿qué sucede cuando un usuario bien intencionado ingresa el valor especial? Y no nos metamos en el lío, esto hará que las rutinas de validación de datos. Si el idioma apoyara el concepto nulo, todas las preocupaciones desaparecerían.


Todas las respuestas hasta ahora se centran en por qué null es algo malo, y cómo es útil si un lenguaje puede garantizar que ciertos valores nunca serán nulos.

Luego continúan sugiriendo que sería una buena idea si aplicara la no nulabilidad para todos los valores, lo que se puede hacer si agrega un concepto como Option o Maybe para representar tipos que no siempre tienen un valor definido. Este es el enfoque adoptado por Haskell.

¡Todo es bueno! Pero no impide el uso de tipos explícitamente anulables / no nulos para lograr el mismo efecto. ¿Por qué, entonces, la Opción sigue siendo algo bueno? Después de todo, Scala admite valores anulables (es necesario, por lo que puede funcionar con bibliotecas de Java) pero también admite Options .

P. Entonces, ¿cuáles son los beneficios más allá de poder eliminar por completo los valores nulos de un idioma?

A. Composición

Si usted hace una traducción ingenua de un código nulo

def fullNameLength(p:Person) = { val middleLen = if (null == p.middleName) p.middleName.length else 0 p.firstName.length + middleLen + p.lastName.length }

a código consciente de la opción

def fullNameLength(p:Person) = { val middleLen = p.middleName match { case Some(x) => x.length case _ => 0 } p.firstName.length + middleLen + p.lastName.length }

No hay mucha diferencia! Pero también es una manera terrible de usar Opciones ... Este enfoque es mucho más limpio:

def fullNameLength(p:Person) = { val middleLen = p.middleName map {_.length} getOrElse 0 p.firstName.length + middleLen + p.lastName.length }

O incluso:

def fullNameLength(p:Person) = p.firstName.length + p.middleName.map{length}.getOrElse(0) + p.lastName.length

Cuando comienzas a tratar con la Lista de opciones, se pone aún mejor. Imagina que la lista de people es en sí misma opcional:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

¿Como funciona esto?

//convert an Option[List[Person]] to an Option[S] //where the function f takes a List[Person] and returns an S people map f //find a person named "Joe" in a List[Person]. //returns Some[Person], or None if "Joe" isn''t in the list validPeopleList find (_.firstName == "joe") //returns None if people is None //Some(None) if people is valid but doesn''t contain Joe //Some[Some[Person]] if Joe is found people map (_ find (_.firstName == "joe")) //flatten it to return None if people is None or Joe isn''t found //Some[Person] if Joe is found people flatMap (_ find (_.firstName == "joe")) //return Some(length) if the list isn''t None and Joe is found //otherwise return None people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

El código correspondiente con controles nulos (o incluso elvis?: Operadores) sería dolorosamente largo. El verdadero truco aquí es la operación flatMap, que permite la comprensión anidada de las opciones y las colecciones de una manera que los valores que no se pueden anular nunca pueden alcanzar.


La inconveniencia de tener referencias / punteros puede ser anulable de forma predeterminada.

No creo que este sea el problema principal con nulos, el principal problema con nulos es que pueden significar dos cosas:

  1. La referencia / puntero no está inicializado: el problema aquí es el mismo que la mutabilidad en general. Por un lado, hace que sea más difícil analizar su código.
  2. La variable que es nula en realidad significa algo: este es el caso de los tipos de opciones que realmente se formalizan.

Los lenguajes que admiten tipos de opciones también suelen prohibir o desalentar el uso de variables no inicializadas.

Cómo funcionan los tipos de opciones, incluidas las estrategias para facilitar la comprobación de casos nulos como la coincidencia de patrones.

Para que sean efectivos, los tipos de opciones deben admitirse directamente en el idioma. De lo contrario, se necesita una gran cantidad de código de placa de caldera para simularlos. La coincidencia de patrones y la inferencia de tipos son dos características clave del lenguaje que facilitan el trabajo con los tipos de opciones. Por ejemplo:

En F #:

//first we create the option list, and then filter out all None Option types and //map all Some Option types to their values. See how type-inference shines. let optionList = [Some(1); Some(2); None; Some(3); None] optionList |> List.choose id //evaluates to [1;2;3] //here is a simple pattern-matching example //which prints "1;2;None;3;None;". //notice how value is extracted from op during the match optionList |> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Sin embargo, en un lenguaje como Java sin soporte directo para los tipos de opción, tendríamos algo como:

//here we perform the same filter/map operation as in the F# example. List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>()); List<Integer> filteredList = new ArrayList<Integer>(); for(Option<Integer> op : list) if(op instanceof Some) filteredList.add(((Some<Integer>)op).getValue());

Solución alternativa como el mensaje comiendo nada.

El "mensaje comiendo nulo" de Objective-C no es tanto una solución como un intento de aliviar el dolor de cabeza de la comprobación de nulos. Básicamente, en lugar de lanzar una excepción de tiempo de ejecución al intentar invocar un método en un objeto nulo, la expresión en su lugar se evalúa como nula. Suspendiendo la incredulidad, es como si cada método de instancia comenzara con if (this == null) return null; . Pero luego hay pérdida de información: no se sabe si el método devolvió el valor nulo porque es un valor de retorno válido o porque el objeto es realmente nulo. Es muy parecido a la deglución de excepciones, y no hace ningún progreso al abordar los problemas con nulos descritos anteriormente.