Se null é ruim, por que as linguagens modernas o implementam? [fechadas]

82

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 ..."?

mrpyo
fonte
99
null é ótimo. Eu amo e uso todos os dias.
Pieter B
17
@ PieterB Mas você o usa para a maioria das referências, ou deseja que a maioria das referências não seja nula? O argumento não é que não deve haver dados anuláveis, apenas que eles devem ser explícitos e aceitar.
11
@PieterB Mas quando a maioria não deve ser anulável, não faria sentido tornar a capacidade nula a exceção e não o padrão? Observe que, embora o design usual dos tipos de opção seja forçar a verificação explícita de ausência e descompactação, também é possível ter a conhecida semântica Java / C # / ... para referências anuláveis ​​de aceitação (use como se não for anulável, amplie se nulo). Pelo menos evitaria alguns bugs e faria uma análise estática que reclama muito mais da falta de verificações nulas.
20
WTF está com vocês? De todas as coisas que podem e dão errado com o software, tentar desreferenciar um nulo não é problema. SEMPRE gera um AV / segfault e, portanto, é corrigido. Existe tanto escassez de bugs que você precisa se preocupar com isso? Nesse caso, tenho bastante sobra e nenhum deles invade problemas com referências / ponteiros nulos.
Martin James
13
@ MartinJames "SEMPRE gera um AV / segfault e, portanto, é corrigido" - não, não, não.
detly

Respostas:

97

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 :

Eu chamo de erro do meu bilhão de dólares. Foi a invenção da referência nula em 1965. Naquela época, eu estava projetando o primeiro sistema abrangente de tipos para referências em uma linguagem orientada a objetos (ALGOL W). Meu objetivo era garantir que todo o uso de referências fosse absolutamente seguro, com a verificação realizada automaticamente pelo compilador. Mas não pude resistir à tentação de colocar uma referência nula, simplesmente porque era muito fácil de implementar. Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos.

Ê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:

"Estávamos atrás dos programadores de C ++. Conseguimos arrastar muitos deles até a metade do Lisp." -Guy Steele, co-autor da especificação Java

(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:

As linguagens de programação em geral são muito mais complicadas do que costumavam ser: orientação a objetos, herança e outros recursos ainda não estão sendo pensados ​​do ponto de vista de uma disciplina coerente e cientificamente fundamentada ou uma teoria da correção . Meu postulado original, que venho buscando como cientista a vida inteira, é que se usa o critério de correção como meio de convergir para um design decente de linguagem de programação - um que não crie armadilhas para seus usuários e outros quais os diferentes componentes do programa correspondem claramente aos diferentes componentes de sua especificação, para que você possa argumentar sobre a composição. [...] As ferramentas, incluindo o compilador, devem se basear em alguma teoria sobre o que significa escrever um programa correto. - Entrevista na história oral de Philip L. Frana, 17 de julho de 2002, Cambridge, Inglaterra; Instituto Charles Babbage, Universidade de Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

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 nullsuperado 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:

É uma pena que Cloneable esteja quebrado, mas isso acontece. As APIs Java originais foram feitas muito rapidamente, dentro de um prazo apertado, para atender a uma janela de fechamento do mercado. A equipe original do Java fez um trabalho incrível, mas nem todas as APIs são perfeitas. Cloneable é um ponto fraco, e acho que as pessoas devem estar cientes de suas limitações. -Josh Bloch

(Fonte: http://www.artima.com/intv/bloch13.html )

Doval
fonte
32
Caro downvoter: como posso melhorar minha resposta?
Doval
6
Você realmente não respondeu à pergunta; você só forneceu algumas citações sobre algumas opiniões posteriores e outras sobre o "custo". (Se nulo é um erro de bilhões de dólares, deve não os dólares salvos por MS e Java, implementando-o reduzir essa dívida?)
DougM
29
@DougM O que você espera que eu faça, chame todos os designers de idiomas dos últimos 50 anos e pergunte a ele por que ele implementou nullem 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 querem null, você os oferece null. Você também supõe que eles aceitaram nullestá lhes custando dinheiro.
Doval
3
Se a única resposta que você pode dar é especulativa, declare isso claramente no parágrafo de abertura. (Você perguntou como você poderia melhorar a sua resposta, e eu respondi Qualquer parênteses é meramente comentário você pode se sentir livre para ignorar; isso é o que parêntese são para em Inglês, depois de tudo..)
DougM
7
Essa resposta é razoável; Eu adicionei mais algumas considerações na minha. Observo que ICloneableé igualmente quebrado no .NET; infelizmente, este é um lugar em que as deficiências do Java não foram aprendidas com o tempo.
Eric Lippert
121

Tenho certeza de que designers de linguagens como Java ou C # conheciam problemas relacionados à existência de referências nulas

Claro.

A implementação de um tipo de opção não é realmente muito mais complexa que as referências nulas.

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.

Por que eles decidiram incluí-lo assim mesmo?

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 dizer Nullable<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.

Eric Lippert
fonte
12
+1 para tudo, mas especialmente para trazer genéricos. É fácil esquecer que havia períodos de tempo na história do Java e do C # em que os genéricos não existiam.
Doval
2
Talvez seja uma pergunta idiota (eu sou apenas um graduado em TI) - mas o tipo de opção não pode ser implementado no nível de sintaxe (com o CLR não sabendo nada sobre isso) como uma referência anulável regular que requer verificação "tem valor" antes de usar o código? Eu acredito que os tipos de opção não precisam de nenhuma verificação no tempo de execução.
Mrpyo
2
@ Mrpyo: Claro, essa é uma opção de implementação possível. Nenhuma das outras opções de design desaparece e essa opção de implementação tem muitos prós e contras próprios.
Eric Lippert
1
@ Mrpyo Eu acho que forçar uma verificação "tem valor" não é uma boa idéia. Teoricamente, é uma idéia muito boa, mas, na prática, o IMO traria todos os tipos de verificações vazias, apenas para satisfazer as exceções verificadas do compilador em Java e as pessoas que brincam com catchesisso 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.
NothingsImpossible
2
@voo: Arrays do tipo de referência não anulável são difíceis por vários motivos. Existem muitas soluções possíveis e todas elas impõem custos em diferentes operações. A sugestão da Supercat é rastrear se um elemento pode ser lido legalmente antes de ser atribuído, o que impõe custos. O seu é garantir que um inicializador seja executado em cada elemento antes que a matriz seja visível, o que impõe um conjunto diferente de custos. Então, aqui está o problema: não importa qual dessas técnicas se escolha, alguém vai reclamar que não é eficiente para o seu cenário de estimação. Este é um ponto sério contra o recurso.
Eric Lippert
28

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 ...

  • Nulo , tem implicações e preocupações das quais você precisa ter cuidado, tentar manipulá-lo acidentalmente sem confirmar que não é nulo primeiro, por exemplo, lançaria uma exceção, mas também representa melhor a situação real ... Se a pessoa não estiver morta a data da morte não existe ... não é nada ... é nula ...
  • 0/0/0000 , isso pode ser bom em alguns idiomas e pode até ser uma representação apropriada sem data. Infelizmente, alguns idiomas e validação rejeitarão isso como um data / hora inválido, o que o torna um impedimento em muitos casos.
  • 1/1/1869 (ou qualquer que seja o valor padrão da data e hora) , o problema aqui é que é complicado lidar com isso. Você pode usar isso como valor de sua falta de valor, exceto o que acontece se eu quiser filtrar todos os meus registros pelos quais não tenho data de falecimento? Eu poderia filtrar facilmente as pessoas que realmente morreram nessa data, o que poderia causar problemas de integridade dos dados.

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)

RualStorge
fonte
37
Null serves a very valid purpose of representing a lack of value.Um tipo Optionou Maybeserve a esse propósito muito válido sem ignorar o sistema de tipos.
Doval
34
Ninguém está argumentando que não deve haver um valor de falta de valor, eles estão argumentando que valores que podem estar ausentes devem ser explicitamente marcados como tal, em vez de todo valor estar potencialmente ausente.
2
Eu acho que o RualStorge estava falando em relação ao SQL, porque existem campos que afirmam que todas as colunas devem ser marcadas como NOT NULL. Minha pergunta não foi relacionada com RDBMS embora ...
mrpyo
5
+1 para distinguir entre "sem valor" e "valor desconhecido"
David
2
Não faria mais sentido separar o estado de uma pessoa? Ou seja, Um Persontipo tem um statecampo de tipo State, que é uma união discriminada de Alivee Dead(dateOfDeath : Date).
precisa
10

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. ..

Telastyn
fonte
3
O primeiro parágrafo não faz sentido para mim. O Java não tem interoperabilidade C na forma sugerida (existe o JNI, mas ele já salta por uma dúzia de argolas para tudo referente a referências; além de raramente ser usado na prática), o mesmo para outras linguagens "modernas".
@ delnan - desculpe, eu estou mais familiarizado com c #, que tem esse tipo de interoperabilidade. Eu presumi que muitas das bibliotecas Java fundamentais também usam JNI na parte inferior.
Telastyn
6
Você faz um bom argumento para permitir nulo, mas ainda pode permitir nulo sem incentivá- lo. Scala é um bom exemplo disso. Ele pode interoperar perfeitamente com APIs Java que usam nulo, mas você deve envolvê-lo em um Optionpara uso no Scala, o que é tão fácil quanto val x = Option(possiblyNullReference). Na prática, não leva muito tempo para as pessoas verem os benefícios de um Option.
Karl Bielefeldt
1
Os tipos de opção andam de mãos dadas com a correspondência de padrões (estaticamente verificada), que o C # infelizmente não possui. F # faz, porém, e é maravilhoso.
Steven Evers
1
@SteveEvers É possível falsificá-lo usando uma classe base abstrata com construtor privado, classes internas seladas e um Matchmétodo que usa delegados como argumentos. Em seguida, você passa expressões lambda para Match(pontos de bônus para usar argumentos nomeados) e Matchchama a correta.
Doval
7

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 nullreferê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 similar null.

Por quê ? Bem, qual é a alternativa?

Em linguagens funcionais como Haskell, você tem o tipo Optionou Maybe; no entanto, essas são baseadas em:

  • tipos paramétricos
  • tipos de dados algébricos

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á. nullComo é fácil, os tipos de dados algébricos paramétricos são mais difíceis. As pessoas optaram pela alternativa mais simples.

Matthieu M.
fonte
+1 para "nulo é fácil, tipos de dados algébricos paramétricos são mais difíceis". Mas acho que não se tratava tanto de tipografia paramétrica e ADTs sendo mais difíceis; é que eles não são percebidos como necessários. Se o Java tivesse sido enviado sem um sistema de objetos, por outro lado, teria fracassado; OOP era um recurso de "showstopping", pois se você não o tivesse, ninguém estava interessado.
Doval
@Doval: bem, OOP pode ter sido necessário para Java, mas não era para C :) Mas é verdade que Java pretendia ser simples. Infelizmente, as pessoas parecem assumir que uma linguagem simples leva a programas simples, o que é meio estranho (Brainfuck é uma linguagem muito simples ...), mas certamente concordamos que linguagens complicadas (C ++ ...) também não são uma panacéia. eles podem ser incrivelmente úteis.
Matthieu M.
1
@ MatthieuM .: Sistemas reais são complexos. Uma linguagem bem projetada cujas complexidades correspondem ao sistema do mundo real sendo modelado pode permitir que o sistema complexo seja modelado com código simples. Tentativas de simplificar demais uma linguagem simplesmente aumentam a complexidade para o programador que a está usando.
Supercat
@ supercat: Eu não poderia concordar mais. Ou, como Einstein é parafraseado: "Torne tudo o mais simples possível, mas não o mais simples".
Matthieu M.
@MatthieuM .: Einstein era sábio de várias maneiras. Os idiomas que tentam assumir "tudo é um objeto, uma referência à qual pode ser armazenado 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 único Objecttipo para tudo não elimina a necessidade de tais distinções; apenas torna mais difícil usá-los corretamente.
Supercat
5

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.

BT
fonte
1
Um dos maiores pontos fortes do Java na perspectiva da implementação, mas pontos fracos na perspectiva da linguagem, é que existe apenas um tipo não primitivo: a Referência de Objeto Promíscuo. Isso simplifica enormemente o tempo de execução, possibilitando algumas implementações de JVM extremamente leves. Esse design, no entanto, significa que todo tipo deve ter um valor padrão e, para uma Referência de objeto promíscuo, o único padrão possível é null.
Supercat
Bem, de qualquer forma, um tipo não primitivo raiz . Por que isso é uma fraqueza do ponto de vista da linguagem? Não entendo por que esse fato exige que cada tipo tenha um valor padrão (ou, inversamente, por que vários tipos de raiz permitiriam que os tipos não tivessem um valor padrão), nem por que isso é uma fraqueza.
BT
Que outro tipo de não-primitivo um elemento de campo ou matriz poderia conter? O ponto fraco é que algumas referências são usadas para encapsular a identidade e outras para encapsular os valores contidos nos objetos identificados por ele. Para variáveis ​​do tipo de referência usadas para encapsular a identidade, 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 ... #
317
... o sistema do tipo Java não tem como expressar isso. Se foomantém a única referência a uma int[]contenção {1,2,3}e o código deseja foomanter uma referência a uma int[]contenção {2,2,3}, a maneira mais rápida de conseguir isso seria incrementar foo[0]. Se o código quiser que um método saiba que fooé válido {1,2,3}, o outro método não modificará a matriz nem persistirá uma referência além do ponto em fooque 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 ...
supercat
... o array pode ser transmitido com segurança como uma referência efêmera, e um método que deseje manter seu valor saberá que é necessário copiá-lo. Na ausência de um tipo desse tipo, as únicas maneiras de expor com segurança o conteúdo de uma matriz são fazer uma cópia ou encapsulá-lo em um objeto criado apenas para esse fim.
Supercat
4

Como as linguagens de programação geralmente são projetadas para serem praticamente úteis, e não tecnicamente corretas. O fato é que os nullestados 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:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

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.

Jack Aidley
fonte
13
E aqui você deu um exemplo do que está exatamente errado com nulos. A função "openfile" implementada corretamente deve gerar uma exceção (para o arquivo ausente) que interromperia a execução com a explicação exata do que aconteceu. Em vez disso, se retornar nulo, ele se propaga mais (para 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.
Mrpyo
2
+1 para "Porque linguagens de programação são geralmente projetado para ser praticamente útil, em vez de tecnicamente correto"
Martin Ba
2
Cada tipo de opção que eu conheço permite que você faça a falha em nulo com uma única chamada de método extra curta (exemplo Rust:) 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.
4
@mrpyo Nem todos os idiomas suportam exceções e / ou tratamento de exceções (a la try / catch). E as exceções também podem ser abusadas - "exceção como controle de fluxo" é um anti-padrão comum. Esse cenário - um arquivo não existe - é o AFAIK o exemplo mais frequentemente mencionado desse antipadrão. Parece que você está substituindo uma prática ruim por outra.
David
8
@mrpyo 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.
4

Existem usos claros e práticos do ponteiro NULL(ou nil, ou Nil, ou null, Nothingou 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:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Aqui, um NULLretorno de malloc(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):

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.
}

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)

Maxthon Chan
fonte
5
O problema é que não há como distinguir variáveis ​​que nunca devem conter nulldaquelas 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.
Doval
@Doval Se essa for a situação, apenas atribua um significado a NULL (ou se você tiver um padrão, trate-o como um sinônimo do valor padrão) ou use o NULL (que nunca deve aparecer em primeiro lugar) como um marcador de erro suave (ou seja, erro, mas pelo menos por enquanto não bater)
Maxthon Chan
1
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), nullnão será possível atribuir um significado que faça sentido para esse tipo. Não há como usar a nullcomo uma estrutura ou uma lista. E, novamente, o problema de usar nullcomo sinal de erro é que não podemos dizer o que pode retornar nulo ou aceitar nulo. Qualquer variável em seu programa pode ser, a nullmenos que você seja extremamente meticuloso para verificar cada uma delas nullantes de cada uso, o que ninguém faz.
Doval
1
@Doval: não haveria nenhuma dificuldade inerente em ter um tipo de referência imutável nullcomo um valor padrão utilizável (por exemplo, ter o valor padrão de stringse 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, calle não callvirtao invocar membros não virtuais.
Supercat 03/03
@ supercat Esse é um bom ponto, mas agora você não precisa adicionar suporte para distinguir entre tipos imutáveis ​​e não imutáveis? Não tenho certeza de quão trivial é adicionar a um idioma.
Doval
4

Existem dois problemas relacionados, mas um pouco diferentes:

  1. Deveria nullexistir? Ou você deve sempre usar Maybe<T>onde nulo é útil?
  2. 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) das nullcausas 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ê precisa nullou 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 da defaultinicialização pode ter superado essas desvantagens.

  • Por exemplo, campos, você pode contornar isso exigindo a inicialização dos campos antes de expor o thisponteiro 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 thisponteiro.

  • 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 retornam default(T)como valuese 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".

CodesInChaos
fonte
O List<T>IMHO é o melhor exemplo, porque exigiria que todos Ttivessem um valor padrão, que todos os itens na loja de backup fossem um Maybe<T>com um campo "isValid" extra, mesmo quando Té um Maybe<U>, ou que o código para o List<T>comportamento se comportasse de maneira diferente dependendo se Té um tipo anulável. Eu consideraria a inicialização dos T[]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.
Supercat3
A ferrugem segue o ponto 1 - nenhum nulo. O Ceilão segue o ponto 2 - não nulo por padrão. Referências que podem ser nulas são declaradas explicitamente com um tipo de união que inclui uma referência ou nula, mas nula nunca pode ser o valor de uma referência simples. Como resultado, o idioma é completamente seguro e não há NullPointerException porque não é semanticamente possível.
Jim Balter
2

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 customersde tipos Customer[20]e tenta Customer[4].GiveMoney(23)sem ter armazenado nada Customer[4], a execução terá que interceptar. Alguém poderia argumentar que uma tentativa de leitura Customer[4]deve interceptar imediatamente, em vez de esperar até que o código tente GiveMoney, 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.

supercat
fonte
Não seria uma Maybe/ OptionTipo 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 armazenar Nothingem um Maybe <Ref type>?
Doval
@Doval: Não, isso não resolveria o problema - pelo menos, não sem a introdução de referências nulas novamente. Um "nada" deve agir como um membro do tipo? Se sim, qual? Ou deveria lançar uma exceção? Nesse caso, como você está melhor do que simplesmente usar nullcorretamente / com sensibilidade?
cHao
@Doval: O tipo de suporte de um List<T>a T[]ou a Maybe<T>? E o tipo de apoio de a List<Maybe<T>>?
Supercat 03/03
@ supercat Eu não tenho certeza de como um tipo de apoio Maybefaz sentido, Listpois Maybemantém um valor único. Você quis dizer Maybe<T>[]?
Doval
O @cHao Nothingsó pode ser atribuído a valores do tipo Maybe, portanto, não é como atribuir null. Maybe<T>e Tsão dois tipos distintos.
Doval