Penso que o resumo sucinto de por que nulo é indesejável é que estados sem sentido não devem ser representáveis .
Suponha que eu esteja modelando uma porta. Pode estar em um dos três estados: aberto, fechado, mas desbloqueado, e fechado e bloqueado. Agora eu poderia modelá-lo ao longo das linhas de
class Door
private bool isShut
private bool isLocked
e está claro como mapear meus três estados nessas duas variáveis booleanas. Mas isso deixa uma quarta, estado indesejado disponíveis: isShut==false && isLocked==true
. Como os tipos que selecionei como minha representação admitem esse estado, devo dedicar esforço mental para garantir que a classe nunca entre nesse estado (talvez codificando explicitamente um invariante). Por outro lado, se eu estivesse usando uma linguagem com tipos de dados algébricos ou enumerações verificadas que me permitissem definir
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
então eu poderia definir
class Door
private DoorState state
e não há mais preocupações. O sistema de tipos garantirá a existência de apenas três estados possíveis para uma instância class Door
. É nisso que os sistemas de tipos são bons - excluindo explicitamente toda uma classe de erros no tempo de compilação.
O problema null
é que todo tipo de referência obtém esse estado extra em seu espaço que normalmente é indesejável. Uma string
variável pode ser qualquer sequência de caracteres ou esse null
valor extra maluco que não é mapeado no domínio do meu problema. Um Triangle
objeto tem três Point
s, que possuem X
e Y
valores, mas infelizmente o Point
s ou o Triangle
próprio pode ser esse valor nulo louco que não faz sentido para o domínio gráfico em que estou trabalhando. Etc.
Quando você pretende modelar um valor possivelmente inexistente, opte explicitamente por ele. Se a maneira que pretendo modelar as pessoas é que todos Person
têm a FirstName
e a LastName
, mas apenas algumas pessoas têm MiddleName
s, então eu gostaria de dizer algo como
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
onde string
aqui é assumido como um tipo não anulável. Então não há invariantes complicados para estabelecer e nenhum NullReferenceException
s inesperado ao tentar calcular o tamanho do nome de alguém. O sistema de tipos garante que qualquer código que lide com as MiddleName
contas pela possibilidade de existir None
, enquanto qualquer código que lide com o FirstName
possa assumir com segurança que existe um valor lá.
Por exemplo, usando o tipo acima, poderíamos criar esta função boba:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
sem preocupações. Por outro lado, em um idioma com referências anuláveis para tipos como string, assumindo
class Person
private string FirstName
private string MiddleName
private string LastName
você acaba criando coisas como
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
que explode se o objeto Person recebido não tiver o invariante de tudo ser não nulo ou
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)
ou talvez
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
assumindo que o p
primeiro / o último esteja lá, mas o meio pode ser nulo, ou talvez você faça verificações que geram tipos diferentes de exceções, ou quem sabe o que. Todas essas loucas opções de implementação e coisas para se pensar surgem porque existe esse valor representável estúpido que você não quer ou precisa.
Nulo normalmente adiciona complexidade desnecessária. A complexidade é inimiga de todos os softwares e você deve se esforçar para reduzir a complexidade sempre que possível.
(Observe bem que há mais complexidade até para esses exemplos simples. Mesmo que a FirstName
não possa ser null
, a string
pode representar ""
(a sequência vazia), que provavelmente também não é um nome de pessoa que pretendemos modelar. Como tal, mesmo com não- seqüências anuláveis, ainda pode ser o caso de estarmos "representando valores sem sentido". Novamente, você pode optar por combater isso por meio de invariantes e código condicional em tempo de execução ou usando o sistema de tipos (por exemplo, para ter um NonEmptyString
tipo). este último talvez seja desaconselhável (tipos "bons" geralmente são "fechados" em um conjunto de operações comuns e, por exemplo, NonEmptyString
não são fechados em.SubString(0,0)
), mas demonstra mais pontos no espaço de design. No final do dia, em qualquer sistema de tipos, existe alguma complexidade da qual será muito bom se livrar, e outra complexidade que é intrinsecamente mais difícil de se livrar. A chave para este tópico é que, em quase todos os sistemas de tipos, a mudança de "referências anuláveis por padrão" para "referências não anuláveis por padrão" é quase sempre uma mudança simples que torna o sistema de tipos muito melhor na luta contra a complexidade e descartando certos tipos de erros e estados sem sentido. Portanto, é muito louco que tantos idiomas continuem repetindo esse erro repetidamente.)