Creo que el resumen sucinto de por qué nulo es indeseable es que los estados sin sentido no deberían ser representables .
Supongamos que estoy modelando una puerta. Puede estar en uno de tres estados: abierto, cerrado pero desbloqueado, y cerrado y bloqueado. Ahora podría modelarlo siguiendo las líneas de
class Door
private bool isShut
private bool isLocked
y está claro cómo mapear mis tres estados en estas dos variables booleanas. Pero esto deja un cuarto estado no deseado disponibles: isShut==false && isLocked==true
. Debido a que los tipos que he seleccionado como mi representación admiten este estado, debo hacer un esfuerzo mental para asegurar que la clase nunca entre en este estado (tal vez codificando explícitamente una invariante). Por el contrario, si estuviera usando un lenguaje con tipos de datos algebraicos o enumeraciones verificadas 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 class Door
. Esto es en lo que son buenos los sistemas de tipos, descartando explícitamente toda una clase de errores en tiempo de compilación.
El problema con null
es que cada tipo de referencia obtiene este estado adicional en su espacio que normalmente no es deseado. Una string
variable podría ser cualquier secuencia de caracteres, o podría ser este loco null
valor extra que no se asigna a mi dominio problemático. Un Triangle
objeto tiene tres Point
s, que ellos mismos tienen X
y Y
valores, pero desafortunadamente el Point
s o el Triangle
mismo podría ser este loco valor nulo 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, entonces debe optar por él explícitamente. Si la forma en que pretendo modelar a las personas es que cada Person
una tiene a FirstName
y a LastName
, pero solo algunas personas tienen MiddleName
s, entonces me gustaría decir algo como
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
donde string
aquí se supone que es un tipo no anulable. Entonces no hay invariantes difíciles de establecer y no hay NullReferenceException
s inesperados al tratar de calcular la longitud del nombre de alguien. El sistema de tipos asegura que cualquier código que se ocupe de las MiddleName
cuentas tiene la posibilidad de serlo None
, mientras que cualquier código que se ocupe de FirstName
eso puede asumir con seguridad que hay 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 Por el contrario, en un lenguaje con referencias anulables para tipos como string, luego suponiendo
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 la invariante de que todo sea no 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
asumiendo que eso p
asegura que primero / último estén allí, pero el medio puede ser nulo, o tal vez haga controles que arrojen diferentes tipos de excepciones, o quién sabe qué. Todas estas locas opciones de implementación y cosas en las que pensar surgen porque existe este estúpido valor representable que no quieres ni necesitas.
Nulo típicamente agrega complejidad innecesaria. La complejidad es el enemigo de todo software, y debe esforzarse por reducir la complejidad siempre que sea razonable.
(Tenga en cuenta que hay incluso más complejidad para incluso estos ejemplos simples. Incluso si un FirstName
no puede ser null
, un string
puede representar ""
(la cadena vacía), que probablemente tampoco sea un nombre de persona que pretendemos modelar. Como tal, incluso con cadenas anulables, aún podría ser el caso de que estamos "representando valores sin sentido". Una vez más, puede optar por combatir esto ya sea a través de invariantes y código condicional en tiempo de ejecución, o mediante el uso del sistema de tipos (por ejemplo, para tener un NonEmptyString
tipo). esto último es quizás desaconsejado (los tipos "buenos" a menudo se "cierran" en un conjunto de operaciones comunes, y p. ej.NonEmptyString
no se cierra.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, y otra complejidad que es intrínsecamente más difícil de eliminar. La clave para 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 descartando ciertos tipos de errores y estados sin sentido. Por lo tanto, es una locura que tantos idiomas sigan repitiendo este error una y otra vez).