Tenho certeza de que designers de linguagens como Java ou C # conheciam problemas relacionados à existência de referências nulas (consulte Referências nulas são realmente ruins? ). A implementação de um tipo de opção não é realmente muito mais complexa que as referências nulas.
Por que eles decidiram incluí-lo assim mesmo? Tenho certeza de que a falta de referências nulas incentivaria (ou forçaria) um código de melhor qualidade (especialmente um melhor design de biblioteca), tanto dos criadores quanto dos usuários.
É simplesmente por causa do conservadorismo - "outras línguas têm, temos que ter também ..."?
language-design
null
mrpyo
fonte
fonte
Respostas:
Isenção de responsabilidade: Como eu não conheço nenhum designer de idiomas pessoalmente, qualquer resposta que eu der será especulativa.
Do próprio Tony Hoare :
Ênfase minha.
Naturalmente, não lhe parecia uma má ideia na época. É provável que tenha sido perpetuado em parte pelo mesmo motivo - se isso pareceu uma boa ideia para o inventor do quicksort, vencedor do Turing Award, não é de surpreender que muitas pessoas ainda não entendam por que isso é mau. Também é provável, em parte, porque é conveniente que os novos idiomas sejam semelhantes aos idiomas mais antigos, por razões de marketing e de curva de aprendizado. Caso em questão:
(Fonte: http://www.paulgraham.com/icad.html )
E, é claro, o C ++ tem nulo porque C é nulo, e não há necessidade de entrar no impacto histórico do C. O C # substituiu o J ++, que foi a implementação do Java pela Microsoft, e também substituiu o C ++ como a linguagem de escolha para o desenvolvimento do Windows, para que pudesse ser obtido em qualquer um.
EDIT Aqui está outra citação de Hoare que vale a pena considerar:
Mais uma vez, enfatize a minha. Sun / Oracle e Microsoft são empresas, e o resultado final de qualquer empresa é dinheiro. Os benefícios para eles de ter
null
superado os contras, ou podem simplesmente ter um prazo muito apertado para considerar completamente o problema. Como exemplo de um erro de linguagem diferente que provavelmente ocorreu devido a prazos:(Fonte: http://www.artima.com/intv/bloch13.html )
fonte
null
em seu idioma? Qualquer resposta a esta pergunta será especulativa, a menos que seja de um designer de linguagem. Não conheço ninguém que frequente este site além de Eric Lippert. A última parte é um arenque vermelho por várias razões. A quantidade de código de terceiros gravada sobre as APIs da MS e Java obviamente supera a quantidade de código na própria API. Então, se seus clientes queremnull
, você os oferecenull
. Você também supõe que eles aceitaramnull
está lhes custando dinheiro.ICloneable
é igualmente quebrado no .NET; infelizmente, este é um lugar em que as deficiências do Java não foram aprendidas com o tempo.Claro.
Eu peço desculpa mas não concordo! As considerações de design que entraram em tipos de valor anuláveis em C # 2 eram complexas, controversas e difíceis. Eles levaram as equipes de design das linguagens e do tempo de execução por muitos meses de debate, implementação de protótipos etc. e, de fato, a semântica do boxe anulável foi alterada muito perto do envio do C # 2.0, o que foi muito controverso.
Todo design é um processo de escolha entre muitos objetivos sutil e grosseiramente incompatíveis; Só posso fazer um breve esboço de apenas alguns dos fatores que seriam considerados:
A ortogonalidade dos recursos da linguagem é geralmente considerada uma coisa boa. O C # possui tipos de valores anuláveis, tipos de valores não anuláveis e tipos de referência anuláveis. Os tipos de referência não anuláveis não existem, o que torna o sistema de tipos não ortogonal.
A familiaridade com os usuários existentes de C, C ++ e Java é importante.
A fácil interoperabilidade com o COM é importante.
A fácil interoperabilidade com todas as outras linguagens .NET é importante.
A fácil interoperabilidade com bancos de dados é importante.
A consistência da semântica é importante; se tivermos referência TheKingOfFrance igual a null, isso sempre significa "não existe rei da França no momento" ou também pode significar "definitivamente existe um rei da França; eu simplesmente não sei quem é agora"? ou pode significar "a própria noção de ter um rei na França não faz sentido, então nem faça a pergunta!"? Nulo pode significar todas essas coisas e muito mais em C #, e todos esses conceitos são úteis.
O custo do desempenho é importante.
Ser passível de análise estática é importante.
A consistência do sistema de tipos é importante; podemos sempre saber que uma referência não anulável nunca é, sob nenhuma circunstância, considerada inválida? E o construtor de um objeto com um campo não anulável do tipo de referência? E no finalizador de um objeto desse tipo, onde o objeto é finalizado porque o código que deveria preencher a referência lançou uma exceção ? Um sistema de tipos que mente para você sobre suas garantias é perigoso.
E quanto à consistência da semântica? Valores nulos se propagam quando usados, mas referências nulas lançam exceções quando usadas. Isso é inconsistente; Essa inconsistência é justificada por algum benefício?
Podemos implementar o recurso sem interromper outros recursos? Quais outros possíveis recursos futuros o recurso impede?
Você vai à guerra com o exército que você tem, não com o que você gostaria. Lembre-se, o C # 1.0 não possui genéricos, portanto, falar
Maybe<T>
como alternativa é um completo não-inicializador. O .NET deveria ter caído por dois anos enquanto a equipe de tempo de execução adicionou genéricos, apenas para eliminar referências nulas?E a consistência do sistema de tipos? Você pode dizer
Nullable<T>
para qualquer tipo de valor - não, espere, isso é mentira. Você não pode dizerNullable<Nullable<T>>
. Você deveria ser capaz? Em caso afirmativo, quais são as semânticas desejadas? Vale a pena fazer com que todo o sistema de tipos tenha um caso especial apenas para esse recurso?E assim por diante. Essas decisões são complexas.
fonte
catches
isso não fazem nada. Eu acho que é melhor deixar o sistema explodir em vez de continuar a operação em um estado possivelmente inválido.Nulo serve a um propósito muito válido de representar uma falta de valor.
Eu direi que sou a pessoa mais vocal que conheço sobre os abusos do nulo e todas as dores de cabeça e sofrimento que eles podem causar, especialmente quando usados generosamente.
Minha posição pessoal é que as pessoas podem usar nulos única quando eles podem justificar que é necessário e adequado.
Exemplo que justifica nulos:
Data da morte normalmente é um campo anulável. Existem três situações possíveis com a data da morte. A pessoa morreu e a data é conhecida, a pessoa morreu e a data é desconhecida ou a pessoa não está morta e, portanto, não existe uma data de morte.
Date of Death também é um campo DateTime e não possui um valor "desconhecido" ou "vazio". Ele tem a data padrão que aparece quando você cria um novo datetime que varia de acordo com o idioma utilizado, mas existe tecnicamente uma chance de que a pessoa de fato morra naquele momento e sinalize como seu "valor vazio" se você quiser use a data padrão.
Os dados precisariam representar a situação corretamente.
A pessoa com data de falecimento da morte é conhecida (9/3/1984)
Simples, '9/3/1984'
A pessoa está morta, a data da morte é desconhecida
Então, o que é melhor? Nulo , '0/0/0000' ou '01 / 01/1869 '(ou qualquer que seja o seu valor padrão?)
Pessoa não está morta data da morte não é aplicável
Então, o que é melhor? Nulo , '0/0/0000' ou '01 / 01/1869 '(ou qualquer que seja o seu valor padrão?)
Então, vamos pensar em cada valor ...
O fato é que às vezes você Não precisa representam nada e certeza às vezes um tipo de variável funciona bem para isso, mas muitas vezes os tipos de variáveis precisam ser capazes de representar nada.
Se não tenho maçãs, tenho 0 maçãs, mas e se não souber quantas maçãs tenho?
Por todos os meios, o nulo é abusado e potencialmente perigoso, mas às vezes é necessário. É apenas o padrão em muitos casos, porque até eu fornecer um valor a falta de um valor e algo precisa representá-lo. (Nulo)
fonte
Null serves a very valid purpose of representing a lack of value.
Um tipoOption
ouMaybe
serve a esse propósito muito válido sem ignorar o sistema de tipos.NOT NULL
. Minha pergunta não foi relacionada com RDBMS embora ...Person
tipo tem umstate
campo de tipoState
, que é uma união discriminada deAlive
eDead(dateOfDeath : Date)
.Eu não iria tão longe quanto "outras línguas têm, temos que ter também ..." como se fosse algum tipo de acompanhamento dos Jones. Um recurso importante de qualquer novo idioma é a capacidade de interoperar com as bibliotecas existentes em outros idiomas (leia-se: C). Como C possui ponteiros nulos, a camada de interoperabilidade precisa necessariamente do conceito de nulo (ou algum outro equivalente "não existe" que exploda quando você o usa).
O designer do idioma poderia ter escolhido usar os Tipos de opção e forçá-lo a manipular o caminho nulo em todos os lugares em que as coisas poderiam ser nulas. E isso quase certamente levaria a menos erros.
Mas (especialmente para Java e C # devido ao tempo de sua introdução e seu público-alvo), o uso de tipos de opção para essa camada de interoperabilidade provavelmente prejudicaria se não prejudicasse sua adoção. O tipo de opção é passado até o fim, irritando muito os programadores de C ++ de meados ao final dos anos 90 - ou a camada de interoperabilidade lançaria exceções ao encontrar nulos, irritando os programadores de C ++ de meados ao final dos anos 90. ..
fonte
Option
para uso no Scala, o que é tão fácil quantoval x = Option(possiblyNullReference)
. Na prática, não leva muito tempo para as pessoas verem os benefícios de umOption
.Match
método que usa delegados como argumentos. Em seguida, você passa expressões lambda paraMatch
(pontos de bônus para usar argumentos nomeados) eMatch
chama a correta.Antes de tudo, acho que todos podemos concordar que um conceito de nulidade é necessário. Existem algumas situações em que precisamos representar a ausência de informações.
Permitir
null
referências (e ponteiros) é apenas uma implementação desse conceito, e possivelmente a mais popular, embora se saiba que há problemas: C, Java, Python, Ruby, PHP, JavaScript, ... todos usam um similarnull
.Por quê ? Bem, qual é a alternativa?
Em linguagens funcionais como Haskell, você tem o tipo
Option
ouMaybe
; no entanto, essas são baseadas em:Agora, o C, Java, Python, Ruby ou PHP original suportavam algum desses recursos? Não. Os genéricos falhos de Java são recentes na história da linguagem e, de alguma forma, duvido que os outros os implementem.
Aí está.
null
Como é fácil, os tipos de dados algébricos paramétricos são mais difíceis. As pessoas optaram pela alternativa mais simples.fonte
Object
" falham em reconhecer que aplicativos práticos precisam de objetos mutáveis não compartilhados e objetos imutáveis compartilháveis (os quais devem se comportar como valores), assim como compartilháveis e não compartilháveis entidades. Usar um únicoObject
tipo para tudo não elimina a necessidade de tais distinções; apenas torna mais difícil usá-los corretamente.Nulo / nulo / nenhum em si não é mau.
Se você assistir ao seu famoso discurso enganador chamado "The Billion dollar Mistake", Tony Hoare fala sobre como permitir que qualquer variável seja capaz de manter nulo foi um grande erro. A alternativa - usando Opções - na verdade não se livra de referências nulas. Em vez disso, permite especificar quais variáveis podem manter nulas e quais não.
De fato, com linguagens modernas que implementam o tratamento adequado de exceções, os erros de desreferência nula não são diferentes de nenhuma outra exceção - você encontra, corrige. Algumas alternativas para referências nulas (o padrão Objeto Nulo, por exemplo) ocultam erros, fazendo com que as coisas falhem silenciosamente até muito mais tarde. Na minha opinião, é muito melhor falhar rapidamente .
Portanto, a questão é: por que os idiomas não conseguem implementar as Opções? De fato, a linguagem indiscutivelmente mais popular de todos os tempos C ++ tem a capacidade de definir variáveis de objeto que não podem ser atribuídas
NULL
. Esta é uma solução para o "problema nulo" Tony Hoare mencionado em seu discurso. Por que a próxima linguagem digitada mais popular, Java, não possui? Pode-se perguntar por que ele tem tantas falhas em geral, especialmente em seu sistema de tipos. Eu não acho que você pode realmente dizer que os idiomas cometem sistematicamente esse erro. Alguns fazem, outros não.fonte
null
.null
é o único padrão sensível. As referências usadas para encapsular o valor, no entanto, podem ter um comportamento padrão sensível nos casos em que um tipo teria ou poderia construir uma instância padrão sensível. Muitos aspectos de como as referências devem se comportar dependem de como e como eles encapsulam o valor, mas ... #foo
mantém a única referência a umaint[]
contenção{1,2,3}
e o código desejafoo
manter uma referência a umaint[]
contenção{2,2,3}
, a maneira mais rápida de conseguir isso seria incrementarfoo[0]
. Se o código quiser que um método saiba quefoo
é válido{1,2,3}
, o outro método não modificará a matriz nem persistirá uma referência além do ponto emfoo
que desejaria modificá-la, a maneira mais rápida de conseguir isso seria passar uma referência à matriz. Se o Java teve um tipo de "efêmero só de leitura de referência", então ...Como as linguagens de programação geralmente são projetadas para serem praticamente úteis, e não tecnicamente corretas. O fato é que os
null
estados são uma ocorrência comum devido a dados incorretos ou ausentes ou a um estado que ainda não foi decidido. As soluções tecnicamente superiores são todas mais difíceis do que simplesmente permitir estados nulos e sugar o fato de que os programadores cometem erros.Por exemplo, se eu quiser escrever um script simples que funcione com um arquivo, posso escrever pseudocódigo como:
e simplesmente falhará se joebloggs.txt não existir. O problema é que, para scripts simples que provavelmente estão bem, e para muitas situações em código mais complexo, eu sei que ele existe e a falha não ocorrerá, forçando-me a verificar desperdício de tempo. As alternativas mais seguras alcançam sua segurança, forçando-me a lidar corretamente com o estado de falha em potencial, mas muitas vezes não quero fazer isso, só quero continuar.
fonte
for line in file
) e lança uma exceção de referência nula sem sentido, o que é bom para um programa tão simples, mas causa problemas reais de depuração em sistemas muito mais complexos. Se nulos não existissem, o designer de "openfile" não seria capaz de cometer esse erro.let file = something(...).unwrap()
. Dependendo do seu POV, é uma maneira fácil de não manipular erros ou uma afirmação sucinta de que nulo não pode ocorrer. O tempo perdido é mínimo e você economiza tempo em outros lugares porque não precisa descobrir se algo pode ser nulo. Outra vantagem (que por si só vale a chamada extra) é que você ignora explicitamente o caso de erro; quando falha, há poucas dúvidas sobre o que deu errado e para onde a correção precisa ir.if file exists { open file }
sofre de uma condição de corrida. A única maneira confiável de saber se a abertura de um arquivo será bem-sucedida é tentar abri-lo.Existem usos claros e práticos do ponteiro
NULL
(ounil
, ouNil
, ounull
,Nothing
ou o que for chamado no seu idioma preferido).Para os idiomas que não possuem um sistema de exceção (por exemplo, C), um ponteiro nulo pode ser usado como sinal de erro quando um ponteiro deve ser retornado. Por exemplo:
Aqui, um
NULL
retorno demalloc(3)
é usado como um marcador de falha.Quando usado em argumentos de método / função, pode indicar o uso padrão para o argumento ou ignorar o argumento de saída. Exemplo abaixo.
Mesmo para os idiomas com mecanismo de exceção, um ponteiro nulo pode ser usado como indicação de erro leve (ou seja, erros recuperáveis), especialmente quando o tratamento da exceção é caro (por exemplo, Objective-C):
Aqui, o erro leve não causa uma falha no programa se não for pego. Isso elimina o try-catch louco como o Java tem e tem um melhor controle no fluxo do programa, pois os erros de software não são interrompidos (e as poucas exceções duras restantes geralmente não são recuperáveis e não são capturadas)
fonte
null
daquelas que deveriam. Por exemplo, se eu quiser um novo tipo que contenha 5 valores em Java, poderia usar uma enumeração, mas o que recebo é um tipo que pode conter 6 valores (os 5 que eu queria +null
). É uma falha no sistema de tipos.Null
é possível atribuir um significado ao @MaxtonChan quando os valores de um tipo não tiverem dados (por exemplo, valores de enumeração). Assim que seus valores forem mais complicados (por exemplo, uma estrutura),null
não será possível atribuir um significado que faça sentido para esse tipo. Não há como usar anull
como uma estrutura ou uma lista. E, novamente, o problema de usarnull
como sinal de erro é que não podemos dizer o que pode retornar nulo ou aceitar nulo. Qualquer variável em seu programa pode ser, anull
menos que você seja extremamente meticuloso para verificar cada uma delasnull
antes de cada uso, o que ninguém faz.null
como um valor padrão utilizável (por exemplo, ter o valor padrão destring
se comportar como uma string vazia, da mesma forma que no Modelo de Objeto Comum anterior). Tudo o que seria necessário seria o uso de idiomas,call
e nãocallvirt
ao invocar membros não virtuais.Existem dois problemas relacionados, mas um pouco diferentes:
null
existir? Ou você deve sempre usarMaybe<T>
onde nulo é útil?Todas as referências devem ser anuláveis? Caso contrário, qual deve ser o padrão?
Ter que declarar explicitamente tipos de referência anuláveis como
string?
ou semelhantes evitaria a maioria (mas não todas) dasnull
causas dos problemas , sem ser muito diferente do que os programadores estão acostumados.Pelo menos, concordo com você que nem todas as referências devem ser anuláveis. Mas evitar nulo não deixa de ter suas complexidades:
O .NET inicializa todos os campos
default<T>
antes que eles possam ser acessados pelo código gerenciado. Isso significa que, para os tipos de referência, você precisanull
ou algo equivalente e que os tipos de valor podem ser inicializados para algum tipo de zero sem executar o código. Embora ambos apresentem desvantagens graves, a simplicidade dadefault
inicialização pode ter superado essas desvantagens.Por exemplo, campos, você pode contornar isso exigindo a inicialização dos campos antes de expor o
this
ponteiro ao código gerenciado. O Spec # seguiu essa rota, usando sintaxe diferente do encadeamento do construtor em comparação com o C #.Para campos estáticos, garantir que isso seja mais difícil, a menos que você coloque fortes restrições sobre que tipo de código pode ser executado em um inicializador de campos, pois você não pode simplesmente ocultar o
this
ponteiro.Como inicializar matrizes de tipos de referência? Considere um
List<T>
que é apoiado por uma matriz com uma capacidade maior que o comprimento. Os elementos restantes precisam ter algum valor.Outro problema é que ele não permite métodos como os
bool TryGetValue<T>(key, out T value)
que retornamdefault(T)
comovalue
se não encontrassem nada. Embora, neste caso, seja fácil argumentar que o parâmetro out é um design ruim, em primeiro lugar, e esse método deve retornar uma união discriminadora ou talvez um substituto.Todos esses problemas podem ser resolvidos, mas não é tão fácil quanto "proibir nulo e tudo está bem".
fonte
List<T>
IMHO é o melhor exemplo, porque exigiria que todosT
tivessem um valor padrão, que todos os itens na loja de backup fossem umMaybe<T>
com um campo "isValid" extra, mesmo quandoT
é umMaybe<U>
, ou que o código para oList<T>
comportamento se comportasse de maneira diferente dependendo seT
é um tipo anulável. Eu consideraria a inicialização dosT[]
elementos com um valor padrão o menos prejudicial dessas opções, mas é claro que isso significa que os elementos precisam ter um valor padrão.As linguagens de programação mais úteis permitem que os itens de dados sejam gravados e lidos em seqüências arbitrárias, de modo que muitas vezes não será possível determinar estaticamente a ordem na qual as leituras e gravações ocorrerão antes da execução do programa. Existem muitos casos em que o código realmente armazena dados úteis em todos os slots antes de lê-los, mas é difícil provar isso. Assim, muitas vezes será necessário executar programas onde seria pelo menos teoricamente possível para o código tentar ler algo que ainda não foi gravado com um valor útil. Seja ou não legal o código fazer isso, não há uma maneira geral de impedir que o código faça a tentativa. A única questão é o que deve acontecer quando isso ocorrer.
Diferentes idiomas e sistemas adotam abordagens diferentes.
Uma abordagem seria dizer que qualquer tentativa de ler algo que não foi escrito provocará um erro imediato.
Uma segunda abordagem é exigir que o código forneça algum valor em todos os locais antes que seja possível lê-lo, mesmo que não haja como o valor armazenado seja semanticamente útil.
Uma terceira abordagem é simplesmente ignorar o problema e deixar o que acontecer "naturalmente" acontecer.
Uma quarta abordagem é dizer que todo tipo deve ter um valor padrão e qualquer slot que não tenha sido gravado com mais nada será padronizado para esse valor.
A abordagem nº 4 é muito mais segura que a abordagem nº 3 e, em geral, é mais barata que as abordagens nº 1 e nº 2. Isso deixa a questão de qual deve ser o valor padrão para um tipo de referência. Para tipos de referência imutáveis, em muitos casos, faria sentido definir uma instância padrão e dizer que o padrão para qualquer variável desse tipo deve ser uma referência a essa instância. Para tipos de referência mutáveis, no entanto, isso não seria muito útil. Se for feita uma tentativa de usar um tipo de referência mutável antes de ser gravada, geralmente não haverá um curso de ação seguro, exceto para interceptar o ponto de tentativa de uso.
Semanticamente falando, se alguém tem uma matriz
customers
de tiposCustomer[20]
e tentaCustomer[4].GiveMoney(23)
sem ter armazenado nadaCustomer[4]
, a execução terá que interceptar. Alguém poderia argumentar que uma tentativa de leituraCustomer[4]
deve interceptar imediatamente, em vez de esperar até que o código tenteGiveMoney
, mas há casos suficientes em que é útil ler um slot, descobrir que ele não possui um valor e depois usá-lo informações, que a falha na tentativa de leitura seria muitas vezes um grande incômodo.Alguns idiomas permitem especificar que determinadas variáveis nunca devem conter nulo, e qualquer tentativa de armazenar um nulo deve desencadear uma interceptação imediata. Esse é um recurso útil. Em geral, porém, qualquer linguagem que permita aos programadores criar matrizes de referências terá que permitir a possibilidade de elementos nulos da matriz ou forçar a inicialização dos elementos da matriz a dados que não podem ser significativos.
fonte
Maybe
/Option
Tipo de resolver o problema com # 2, uma vez que se você não tiver um valor para sua referência ainda , mas vai ter um no futuro, você pode apenas armazenarNothing
em umMaybe <Ref type>
?null
corretamente / com sensibilidade?List<T>
aT[]
ou aMaybe<T>
? E o tipo de apoio de aList<Maybe<T>>
?Maybe
faz sentido,List
poisMaybe
mantém um valor único. Você quis dizerMaybe<T>[]
?Nothing
só pode ser atribuído a valores do tipoMaybe
, portanto, não é como atribuirnull
.Maybe<T>
eT
são dois tipos distintos.