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

78

Tenho certeza de que designers de linguagens como Java ou C # conhecem problemas relacionados à existência de referências nulas (consulte São referências nulas realmente uma coisa ruim? ). Também implementar um tipo de opção não é muito mais complexo do que referências nulas.

Por que eles decidiram incluí-lo mesmo assim? Tenho certeza de que a falta de referências nulas encorajaria (ou até mesmo forçaria) um código de melhor qualidade (especialmente melhor design de biblioteca) tanto de criadores de linguagem quanto de usuários.

É simplesmente por causa do conservadorismo - "outras línguas o têm, também temos que ter ..."?

    
por mrpyo 02.05.2014 / 14:19
fonte

10 respostas

92

Disclaimer: Desde que eu não conheço nenhum designer de linguagem pessoalmente, qualquer resposta que eu lhe der será especulativa.

De Tony Hoare :

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Ênfase minha.

Naturalmente, não pareceu uma má idéia para ele na época. É provável que tenha sido perpetuado em parte pela mesma razão - se pareceu uma boa idéia para o inventor do quicksort, vencedor do Prêmio Turing, não é de surpreender que muitas pessoas ainda não entendam por que é mal. Também é provável, em parte, porque é conveniente que os novos idiomas sejam similares aos idiomas mais antigos, tanto por razões de marketing quanto por razões de aprendizado. Caso em questão:

"We were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp." -Guy Steele, co-author of the Java spec

(Fonte: link )

E, é claro, C ++ tem nulo porque C tem nulo e não há necessidade de entrar no impacto histórico de C. C # tipo de substituído J + +, que foi a implementação da Microsoft de Java, e também é substituída C + + como a linguagem de escolha para o desenvolvimento do Windows, por isso poderia ter obtido de qualquer um.

EDIT Aqui está outra citação de Hoare que vale a pena considerar:

Programming languages on the whole are very much more complicated than they used to be: object orientation, inheritance, and other features are still not really being thought through from the point of view of a coherent and scientifically well-based discipline or a theory of correctness. My original postulate, which I have been pursuing as a scientist all my life, is that one uses the criteria of correctness as a means of converging on a decent programming language design—one which doesn’t set traps for its users, and ones in which the different components of the program correspond clearly to different components of its specification, so you can reason compositionally about it. [...] The tools, including the compiler, have to be based on some theory of what it means to write a correct program. -Oral history interview by Philip L. Frana, 17 July 2002, Cambridge, England; Charles Babbage Institute, University of Minnesota.[ http://www.cbi.umn.edu/oh/display.phtml?id=343]

Mais uma vez, ênfase minha. Sun / Oracle e Microsoft são empresas, e a linha inferior de qualquer empresa é dinheiro. Os benefícios para eles de ter null podem ter superado os contras, ou eles 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:

It's a shame that Cloneable is broken, but it happens. The original Java APIs were done very quickly under a tight deadline to meet a closing market window. The original Java team did an incredible job, but not all of the APIs are perfect. Cloneable is a weak spot, and I think people should be aware of its limitations. -Josh Bloch

(Fonte: link )

    
por 02.05.2014 / 14:38
fonte
119

I'm sure designers of languages like Java or C# knew issues related to existence of null references

Claro.

Also implementing an option type isn't really much more complex than null references.

Eu imploro para diferir! As considerações de design que entraram em tipos de valor anulável em C # 2 foram complexas, controversas e difíceis. Eles levaram as equipes de design das linguagens e da execução muitos meses de debate, implementação de protótipos e assim por diante, e de fato a semântica do boxe anulável foi mudada muito próximo ao envio de C # 2.0, o que foi muito controverso. p>

Why did they decide to include it anyway?

Todo o design é um processo de escolha entre muitas metas sutilmente e grosseiramente incompatíveis; Eu só posso dar um breve esboço de apenas alguns dos fatores que seriam considerados:

  • A ortogonalidade dos recursos de linguagem é geralmente considerada uma coisa boa. C # tem tipos de valor anuláveis, tipos de valor não anuláveis e tipos de referência anuláveis. Tipos de referência não anuláveis não existem, o que torna o sistema de tipos não ortogonal.

  • Familiaridade com usuários existentes de C, C ++ e Java é importante.

  • A interoperabilidade fácil com o COM é importante.

  • A interoperabilidade fácil com todas as outras linguagens .NET é importante.

  • A interoperabilidade fácil 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 há rei da França agora", ou também pode significar "há definitivamente 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 é absurda, então nem faça a pergunta!" Nulo pode significar todas essas coisas e mais em C #, e todos esses conceitos são úteis.

  • O custo de 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 em qualquer circunstância observada como inválida? E quanto ao construtor de um objeto com um campo de tipo de referência não anulável? Que tal no finalizador de tal objeto, onde o objeto é finalizado porque o código que deveria preencher a referência lançou uma exceção ? Um sistema de tipos que se refere a você sobre suas garantias é perigoso.

  • E a consistência da semântica? Valores nulos propagam-se quando usados, mas as referências nulas lançam exceções quando usados. Isso é inconsistente; é essa inconsistência justificada por algum benefício?

  • Podemos implementar o recurso sem violar outros recursos? Que outros recursos futuros possíveis o recurso impede?

  • Você vai para a guerra com o exército que você tem, não com o que você gostaria. Lembre-se, C # 1.0 não tem genéricos, então falar sobre Maybe<T> como alternativa é um completo não-inicial. O .NET deve ter caído por dois anos enquanto a equipe de tempo de execução adicionou genéricos, apenas para eliminar referências nulas?

  • E quanto à consistência do sistema de tipos? Você pode dizer Nullable<T> para qualquer tipo de valor - não, espere, isso é uma mentira. Você não pode dizer Nullable<Nullable<T>> . Você deve ser capaz? Se sim, 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.

    
por 02.05.2014 / 23:13
fonte
27

O nulo tem um propósito muito válido de representar uma falta de valor.

Eu vou dizer 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 liberalmente.

Meu ponto de vista pessoal é que as pessoas podem usar nulos somente quando puderem justificar que é necessário e apropriado.

Exemplo justificando nulos:

A data da morte é tipicamente um campo anulável. Existem três situações possíveis com a data da morte. Ou a pessoa morreu e a data é conhecida, a pessoa morreu e a data é desconhecida, ou a pessoa não está morta e, portanto, a data da morte não existe.

A data da morte também é um campo DateTime e não tem um valor "desconhecido" ou "vazio". Ele tem a data padrão que surge quando você faz um novo datetime que varia de acordo com o idioma utilizado, mas tecnicamente há 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 adequadamente.

Pessoa é a data de falecimento morta é conhecida (3/9/1984)

Simples, "3/9/1984"

Pessoa cuja data de morte é desconhecida

Então, o que é melhor? Nulo , '0/0/0000', ou '01 / 01/1869 '(ou seja qual for o valor padrão?)

A pessoa não está com data de morte não aplicável

Então, o que é melhor? Nulo , '0/0/0000', ou '01 / 01/1869 '(ou seja qual for o valor padrão?)

Então, vamos pensar em cada valor ao longo de ...

  • Nulo , tem implicações e preocupações que você precisa ter cuidado, tentando acidentalmente manipulá-lo sem confirmar que ele 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 está morta a data da morte não existe ... não é nada ... é nula ...
  • 0/0/0000 , isso pode estar correto em alguns idiomas e pode até ser uma representação apropriada de nenhuma data. Infelizmente, algumas linguagens e validação irão rejeitar isso como um datetime inválido, o que torna desnecessário em muitos casos.
  • 1/1/1869 (ou qualquer que seja seu valor padrão de data e hora) , o problema aqui é que é difícil de manipular. Você poderia usar isso como sua falta de valor, exceto o que acontece se eu quiser filtrar todos os meus registros para os quais não tenho uma data de morte? Eu poderia facilmente filtrar as pessoas que realmente morreram nessa data, o que poderia causar problemas de integridade de dados.

O fato é que às vezes você Do precisa representar nada e com certeza às vezes um tipo de variável funciona bem para isso, mas frequentemente tipos de variáveis precisam ser capazes de representar nada.

Se eu não tiver maçãs, eu tenho 0 maçãs, mas e se eu não souber quantas maçãs eu 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)

    
por 02.05.2014 / 21:42
fonte
9

Eu não iria tão longe quanto "outras linguagens têm, nós temos que ter também ..." como se fosse algum tipo de acompanhamento com os Jones. Um recurso importante de qualquer novo idioma é a capacidade de interoperar com bibliotecas existentes em outros idiomas (leia: C). Como C tem ponteiros nulos, a camada de interoperabilidade precisa necessariamente do conceito de null (ou algum outro "não existe" equivalente que explode quando você o utiliza).

O designer de idiomas poderia ter escolhido usar Tipos de opções e forçar você a manipular o caminho nulo em todos os lugares que as coisas podem ser nulas. E isso quase certamente levaria a menos bugs.

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 teria prejudicado se não fosse torpedeado sua adoção. Ou o tipo de opção é passado todo o caminho, aborrecendo o inferno de programadores C ++ do meio ao final dos anos 90 - ou a camada de interoperabilidade lançaria exceções ao encontrar nulos, aborrecendo o inferno de programadores C ++ do meio ao final dos anos 90. ..

    
por 02.05.2014 / 14:33
fonte
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 null references (e ponteiros) é apenas uma implementação deste conceito, e possivelmente o mais popular, embora seja conhecido por ter problemas: C, Java, Python, Ruby, PHP, JavaScript, ... todos usam um null semelhante.

Por quê? Bem, qual é a alternativa?

Em linguagens funcionais, como o Haskell, você tem o tipo Option ou Maybe ; no entanto, esses são construídos em cima:

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

Agora, o C original, Java, Python, Ruby ou PHP oferece suporte a esses recursos? Não. Os genéricos defeituosos do Java estão recentes na história da linguagem e eu duvido que os outros os implementem.

Lá você tem. null é fácil, os tipos de dados algébricos paramétricos são mais difíceis. As pessoas optaram pela alternativa mais simples.

    
por 03.05.2014 / 17:19
fonte
4

Como as linguagens de programação geralmente são projetadas para serem úteis em vez de tecnicamente corretas. O fato é que 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 complicadas do que simplesmente permitir estados nulos e sugando 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 ele simplesmente falhará se o joebloggs.txt não existir. O problema é que, para scripts simples, provavelmente está tudo bem, e para muitas situações em código mais complexo, sei que existe e a falha não acontecerá, forçando-me a checar o desperdício de tempo. As alternativas mais seguras alcançam sua segurança, forçando-me a lidar corretamente com o potencial estado de falha, mas muitas vezes eu não quero fazer isso, só quero seguir em frente.

    
por 02.05.2014 / 19:27
fonte
4

Há usos claros e práticos do ponteiro NULL (ou nil , ou Nil , ou null , ou Nothing ou seja lá o que for chamado em seu idioma preferido).

Para aqueles idiomas que não possuem um sistema de exceção (por exemplo, C), um ponteiro nulo pode ser usado como uma marca de erro quando um ponteiro deve ser retornado. Por exemplo:

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

Aqui, um NULL retornado de malloc(3) é usado como um marcador de falha.

Quando usado em argumentos de método / função, pode indicar o padrão de uso 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 temporário (isto é, erros recuperáveis) especialmente quando o tratamento de exceções é 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 de software não faz com que o programa trave se não for pego. Isso elimina o louco try-catch como o Java tem e tem um controle melhor no fluxo do programa, já que os soft errors não estão interrompendo (e as poucas exceções duras restantes geralmente não são recuperáveis e deixadas sem serem detectadas)

    
por 02.05.2014 / 19:43
fonte
4

Existem duas questões relacionadas, mas ligeiramente diferentes:

  1. O null deve existir? Ou você deve sempre usar Maybe<T> onde null é útil?
  2. Todas as referências devem ser anuláveis? Se não, qual deve ser o padrão?

    Ter que declarar explicitamente os tipos de referência anuláveis como string? ou similar evitaria a maioria (mas não todos) dos problemas null , sem ser muito diferente do que os programadores estão acostumados.

Eu pelo menos concordo com você que nem todas as referências devem ser anuláveis. Mas evitar nulo não é sem suas complexidades:

O .NET inicializa todos os campos para default<T> antes que eles possam ser acessados primeiro pelo código gerenciado. Isso significa que para tipos de referência você precisa de null ou algo equivalente e que tipos de valor podem ser inicializados para algum tipo de zero sem executar código. Embora ambos tenham desvantagens graves, a simplicidade da inicialização default pode ter superado essas desvantagens.

  • Para campos de instância , você pode contornar isso exigindo a inicialização de campos antes de expor o ponteiro this ao código gerenciado. A especificação # foi nessa rota, usando uma sintaxe diferente do encadeamento de construtor em comparação com o C #.

  • Para campos estáticos garantindo que isso seja mais difícil, a menos que você represente strongs restrições quanto ao tipo de código que pode ser executado em um inicializador de campo, pois não é possível ocultar o ponteiro this .

  • Como inicializar matrizes de tipos de referência? Considere um List<T> que é apoiado por um array com uma capacidade maior que o comprimento. Os elementos restantes precisam ter um valor algum .

Outro problema é que ele não permite métodos como bool TryGetValue<T>(key, out T value) que retornam default(T) como value caso não encontrem nada. Embora, nesse caso, seja fácil argumentar que o parâmetro out é um design incorreto e, em vez disso, esse método deve retornar uma união discriminatória ou um talvez .

Todos esses problemas podem ser resolvidos, mas não é tão fácil quanto "proibir null e tudo está bem".

    
por 02.05.2014 / 20:39
fonte
4

Nulo / nulo / nenhum em si não é mal.

Se você assistir a seu famoso discurso enganosamente 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 - não não de fato se livrar de referências nulas. Em vez disso, permite que você especifique quais variáveis podem ser mantidas nulas e quais não são.

Na verdade, com linguagens modernas que implementam o tratamento adequado de exceções, os erros de desreferenciamento nulo não são diferentes de qualquer 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 que o falhe rápido .

Portanto, a questão é, por que os idiomas não implementam as opções? De fato, a linguagem sem dúvida 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" que Tony Hoare mencionou em seu discurso. Por que a próxima linguagem mais popular, o Java, não tem? Alguém pode perguntar por que tem tantas falhas em geral, especialmente em seu sistema de tipos. Eu não acho que você possa realmente dizer que as línguas sistematicamente cometem esse erro. Alguns fazem, outros não.

    
por 04.05.2014 / 05:30
fonte
2

A maioria das linguagens de programação úteis permite que 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 de um programa ser executado. Existem muitos casos em que o código armazenará dados úteis em cada slot antes de lê-lo, mas onde isso seria difícil. Assim, muitas vezes será necessário executar programas em que seria pelo menos teoricamente possível que o código tentasse ler algo que ainda não tenha sido escrito com um valor útil. Seja ou não legal para 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 ocorre.

Diferentes idiomas e sistemas adotam abordagens diferentes.

  • Uma abordagem seria dizer que qualquer tentativa de ler algo que não tenha sido escrito causará um erro imediato.

  • Uma segunda abordagem é exigir que o código forneça algum valor em cada local antes que seja possível lê-lo, mesmo que não haja maneira de o valor armazenado ser semanticamente útil.

  • Uma terceira abordagem é simplesmente ignorar o problema e deixar que o que acontece "naturalmente" simplesmente aconteça.

  • Uma quarta abordagem é dizer que cada tipo deve ter um valor padrão, e qualquer slot que não tenha sido escrito com qualquer outra coisa será padronizado para esse valor.

A abordagem 4 é muito mais segura do que a abordagem # 3 e é, em geral, mais barata do que as abordagens # 1 e # 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 ter sido escrito, geralmente não há nenhum curso seguro de ação, exceto para interceptar o ponto de tentativa de uso.

Semanticamente falando, se alguém tiver uma matriz customers do tipo Customer[20] e uma tentativa Customer[4].GiveMoney(23) sem ter armazenado nada em Customer[4] , a execução terá que ser interceptada. Pode-se argumentar que uma tentativa de ler Customer[4] deve ser interceptada 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 tem valor, e, em seguida, fazer uso dessa informação, que ter a tentativa de leitura em si falha seria muitas vezes um grande incômodo.

Algumas linguagens permitem especificar que determinadas variáveis nunca devem conter nulo, e qualquer tentativa de armazenar um nulo deve acionar uma interceptação imediata. Esse é um recurso útil. Em geral, no entanto, qualquer linguagem que permita que programadores criem matrizes de referências terá que permitir a possibilidade de elementos matriciais nulos, ou então forçar a inicialização de elementos de matriz a dados que possivelmente não podem ser significativos.

    
por 03.05.2014 / 05:22
fonte