Por que a maioria das linguagens imperativas / OO "bem conhecidas" permite acesso não verificado a tipos que podem representar um valor "nada"?


29

Eu tenho lido sobre a (des) conveniência de ter em nullvez de (por exemplo) Maybe. Depois de ler este artigo , estou convencido de que seria muito melhor usarMaybe (ou algo semelhante). No entanto, fico surpreso ao ver que todas as linguagens de programação imperativas ou orientadas a objetos "conhecidas" ainda usam null(o que permite acesso não verificado a tipos que podem representar um valor "nada"), e isso Maybeé usado principalmente em linguagens de programação funcionais.

Como exemplo, observe o seguinte código C #:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

Algo cheira mal aqui ... Por que deveríamos verificar se o argumento é nulo? Não devemos assumir que toda variável contém uma referência a um objeto? Como você pode ver, o problema é que, por definição, quase todas as variáveis ​​podem conter uma referência nula. E se pudéssemos decidir quais variáveis ​​são "anuláveis" e quais não? Isso nos pouparia muito esforço ao depurar e procurar uma "NullReferenceException". Imagine que, por padrão, nenhum tipo possa conter uma referência nula . Em vez disso, você declararia explicitamente que uma variável pode conter uma referência nula , apenas se você realmente precisar dela. Essa é a ideia por trás do Talvez. Se você possui uma função que, em alguns casos, falha (por exemplo, divisão por zero), você pode retornar umMaybe<int>, declarando explicitamente que o resultado pode ser um int, mas também nada! Esse é um dos motivos para preferir Talvez, em vez de nulo. Se você estiver interessado em mais exemplos, sugiro ler este artigo .

Os fatos são que, apesar das desvantagens de tornar a maioria dos tipos anuláveis ​​por padrão, a maioria das linguagens de programação OO realmente o faz. É por isso que me pergunto:

  • Que tipo de argumentos você teria que implementar nullem sua linguagem de programação em vez de Maybe? Existem razões ou é apenas "bagagem histórica"?

Certifique-se de entender a diferença entre nulo e Talvez antes de responder a esta pergunta.


3
Sugiro ler sobre Tony Hoare , em particular seu erro de um bilhão de dólares.
Oded

2
Bem, essa foi a razão dele . E o resultado lamentável é que foi um erro copiado para a maioria dos idiomas que se seguiram até hoje. Não são línguas em que nullou o seu conceito não existem (IIRC Haskell é um exemplo).
Oded

9
Bagagem histórica não é algo para subestimar. E lembre-se de que os sistemas operacionais em que foram criados foram escritos em idiomas que possuem nulls por um longo tempo. Não é fácil simplesmente largar isso.
Oded

3
Eu acho que você deveria lançar alguma luz sobre o conceito de Talvez, em vez de postar um link.
Jeffo

1
@ GlenH7 Strings em C # são valores de referência (podem ser nulos). Eu sei que int é um valor primitivo, mas foi útil mostrar o uso de talvez.
aochagavia

Respostas:


15

Eu acredito que é principalmente bagagem histórica.

O idioma mais importante e mais antigo com nullé C e C ++. Mas aqui, nullfaz sentido. Os ponteiros ainda são um conceito bastante numérico e de baixo nível. E como alguém disse, na mentalidade dos programadores de C e C ++, ter que dizer explicitamente que o ponteiro pode ser nullnão faz sentido.

O segundo da fila vem o Java. Considerando que os desenvolvedores de Java estavam tentando se aproximar do C ++, para que pudessem simplificar a transição do C ++ para o Java, provavelmente não queriam mexer com esse conceito principal da linguagem. Além disso, a implementação explícita nullexigiria muito mais esforço, porque você deve verificar se a referência não nula está realmente definida corretamente após a inicialização.

Todos os outros idiomas são iguais ao Java. Eles geralmente copiam da maneira que C ++ ou Java o fazem e, considerando o conceito principal nullde tipos de referência implícitos , torna-se muito difícil projetar uma linguagem que esteja usando explícita null.


“Eles provavelmente não queriam mexer com o conceito principal da linguagem”. Eles já removeram completamente os indicadores, e remover nullnão seria uma grande mudança, eu acho.
svick

2
@svick Para Java, as referências substituem os ponteiros. E, em muitos casos, os ponteiros em C ++ foram usados ​​da mesma maneira que as referências Java. Algumas pessoas até afirmam que o Java tem ponteiros ( programmers.stackexchange.com/questions/207196/… ) #
Euphoric

Eu marquei isso como a resposta correta. Eu gostaria de votar novamente, mas não tenho reputação suficiente.
aochagavia

1
Aceita. Observe que em C ++ (diferente de Java e C), a capacidade de nulo é a exceção. std::stringnão pode ser null. int&não pode ser null. Uma int*lata e o C ++ permitem acesso desmarcado por duas razões: 1. porque C fez e 2. porque você deveria entender o que está fazendo ao usar ponteiros brutos em C ++.
MSalters

@MSalters: se um tipo não tiver um valor padrão copiável de bits, a criação de uma matriz desse tipo exigirá a chamada de um construtor para cada elemento antes de permitir o acesso à própria matriz. Isso pode exigir um trabalho inútil (se alguns ou todos os elementos serão substituídos antes de serem lidos), poderá introduzir complicações se o construtor de um elemento posterior falhar após a construção de um elemento anterior e, no final, não acabar realmente realizando muito (se um valor adequado para alguns elementos da matriz não pôde ser determinado sem a leitura de outros).
Supercat

15

Na verdade, nullé uma ótima idéia. Dado um ponteiro, queremos designar que esse ponteiro não faz referência a um valor válido. Portanto, pegamos um local da memória, declaramos inválido e mantemos a convenção (uma convenção às vezes aplicada com segfaults). Agora, sempre que tenho um ponteiro, posso verificar se ele contém Nothing( ptr == null) ou Some(value)( ptr != null, value = *ptr). Eu quero que você entenda que isso é equivalente a um Maybetipo.

Os problemas com isso são:

  1. Em muitos idiomas, o sistema de tipos não ajuda aqui para garantir uma referência não nula.

    Essa é uma bagagem histórica, pois muitas linguagens imperativas ou OOP convencionais tiveram apenas avanços incrementais em seus sistemas de tipos quando comparadas aos antecessores. Pequenas mudanças têm a vantagem de aprender novos idiomas. C # é uma linguagem convencional que introduziu ferramentas no nível da linguagem para lidar melhor com nulos.

  2. Os designers de API podem retornar nullcom falha, mas não uma referência à coisa real em questão. Freqüentemente, a coisa (sem referência) é retornada diretamente. Esse nivelamento de um nível de ponteiro torna impossível usar nullcomo valor.

    Isso é apenas preguiça do lado do designer e não pode ser ajudado sem impor o aninhamento adequado com um sistema de tipos adequado. Algumas pessoas também podem tentar justificar isso com considerações de desempenho ou com a existência de verificações opcionais (uma coleção pode retornar nullou o próprio item, mas também fornece um containsmétodo).

  3. Em Haskell, há uma visão clara do Maybetipo como mônada. Isso facilita a composição de transformações no valor contido.

    Por outro lado, idiomas de baixo nível como C mal tratam matrizes como um tipo separado, então não tenho certeza do que estamos esperando. Em linguagens OOP com polimorfismo parametrizado, um Maybetipo verificado em tempo de execução é bastante trivial de implementar.


“O C # está afastando etapas de referências nulas.” Quais etapas são essas? Não vi nada parecido nas mudanças de idioma. Ou você quer dizer apenas que as bibliotecas comuns estão usando nullmenos do que no passado?
svick

@svick Frases ruins da minha parte. “ C # é uma linguagem convencional que introduziu ferramentas no nível da linguagem para lidar melhor com nulls ” - estou falando sobre Nullabletipos e o ??operador padrão. No momento, ele não resolve o problema na presença de código legado, mas é um passo em direção a um futuro melhor.
15133 amon

Eu concordo com você. Eu votaria a sua resposta, mas eu não tenho reputação enought :( No entanto, Nullable funciona apenas para tipos primitivos Por isso, é apenas um pequeno passo..
aochagavia

1
@svick Nullable não tem nada a ver com isso. Estamos falando de todos os tipos de referência que permitem implicitamente valor nulo, em vez de o programador defini-lo explicitamente. E Nullable pode ser usado apenas em tipos de valor.
Euphoric

@Euphoric Acho que seu comentário foi feito como uma resposta a amon, eu não mencionei Nullable.
Sv16

9

Meu entendimento é que essa nullera uma construção necessária para abstrair as linguagens de programação do assembly. 1 Os programadores precisavam da capacidade de indicar que um valor de ponteiro ou registro era not a valid valuee nullse tornou o termo comum para esse significado.

Reforçando o ponto que nullé apenas uma convenção para representar um conceito, o valor real nullusado para poder / pode variar com base na linguagem e plataforma de programação.

Se você estava projetando um novo idioma e queria evitar, nullmas usar em maybevez disso, incentivaria um termo mais descritivo, como not a valid valueou navvpara indicar a intenção. Mas o nome desse não valor é um conceito separado do fato de você permitir que não existam valores no seu idioma.

Antes de decidir sobre um desses dois pontos, é necessário definir o significado de maybeseu sistema. Você pode achar que é apenas uma renomeação do nullsignificado de not a valid valueou pode ter uma semântica diferente para o seu idioma.

Da mesma forma, a decisão de verificar o acesso contra nullacesso ou referência é outra decisão de design do seu idioma.

Para fornecer um pouco de história, Chavia uma suposição implícita de que os programadores entendiam o que estavam tentando fazer ao manipular a memória. Como era uma abstração superior à montagem e às linguagens imperativas que a precederam, arriscaria que o pensamento de proteger o programador de uma referência incorreta não tivesse passado pela cabeça deles.

Acredito que alguns compiladores ou suas ferramentas adicionais podem fornecer uma medida de verificação em relação ao acesso inválido ao ponteiro. Portanto, outros observaram esse problema em potencial e tomaram medidas para se proteger.

Se você deve ou não permitir isso, depende do que você deseja que seu idioma cumpra e de qual grau de responsabilidade você deseja atribuir aos usuários do seu idioma. Também depende da sua capacidade de criar um compilador para restringir esse tipo de comportamento.

Então, para responder às suas perguntas:

  1. "Que tipo de argumentos ..." - Bem, depende do que você deseja que o idioma faça. Se você deseja simular o acesso bare-metal, convém permitir.

  2. "é apenas bagagem histórica?" Talvez não. nullcertamente teve / tem significado para vários idiomas e ajuda a direcionar a expressão desses idiomas. O precedente histórico pode ter afetado os idiomas mais recentes e sua permissão, nullmas é um pouco demais para agitar as mãos e declarar o conceito de bagagem histórica inútil.


1 Consulte este artigo da Wikipedia, embora seja dado crédito ao Hoare por valores nulos e linguagens orientadas a objetos. Acredito que as linguagens imperativas progrediram em uma árvore genealógica diferente da de Algol.


O ponto é que a maioria das variáveis ​​em, digamos, C # ou Java, pode receber uma referência nula. Parece que seria muito melhor atribuir referências nulas apenas a objetos que indicam explicitamente que "Talvez" não há referência. Então, minha pergunta é sobre o "conceito" nulo, e não a palavra.
aochagavia

2
“Referências de ponteiro nulas podem aparecer como erros durante a compilação” Quais compiladores podem fazer isso?
svick

Para ser completamente justo, você nunca atribui uma referência nula a um objeto ... a referência ao objeto simplesmente não existe (a referência aponta para nada (0x000000000), que é por definição null).
mgw854

A citação da especificação C99 fala sobre o caractere nulo , não o ponteiro nulo , esses são dois conceitos muito diferentes.
svick

2
@ GlenH7 Não faz isso por mim. O código object o = null; o.ToString();compila perfeitamente para mim, sem erros ou avisos no VS2012. O ReSharper se queixa disso, mas esse não é o compilador.
svick

7

Se você observar os exemplos no artigo que você citou, na maioria das vezes o uso Maybenão diminui o código. Não evita a necessidade de verificar Nothing. A única diferença é que você deve fazer isso através do sistema de tipos.

Note, eu digo "lembre", não force. Programadores são preguiçosos. Se um programador está convencido de que um valor não pode ser Nothing, eles vão desreferenciar o Maybesem checá-lo, assim como desreferenciam um ponteiro nulo agora. O resultado final é que você converte uma exceção de ponteiro nulo em uma exceção "talvez vazio não referenciado".

O mesmo princípio da natureza humana se aplica a outras áreas em que as linguagens de programação tentam forçar os programadores a fazer alguma coisa. Por exemplo, os designers de Java tentaram forçar as pessoas a lidar com a maioria das exceções, o que resultou em muitos clichês que ignoram silenciosamente ou propagam cegamente exceções.

O que Maybeé bom é quando muitas decisões são tomadas por meio de correspondência de padrões e polimorfismo em vez de verificações explícitas. Por exemplo, você pode criar funções separadas processData(Some<T>)e com as processData(Nothing<T>)quais não pode fazer null. Você move automaticamente o tratamento de erros para uma função separada, o que é muito desejável na programação funcional em que as funções são passadas e avaliadas preguiçosamente, em vez de sempre serem chamadas de cima para baixo. No POO, a maneira preferida de desacoplar seu código de tratamento de erros é com exceções.


Você acha que seria um recurso desejável em uma nova linguagem OO?
aochagavia

Você pode implementá-lo você mesmo se quiser obter os benefícios do polimorfismo. A única coisa para a qual você precisa de suporte ao idioma é a não-anulabilidade. Eu adoraria ver uma maneira de especificar isso para você, semelhante a const, mas torná-lo opcional. Alguns tipos de código de nível inferior, como listas vinculadas, por exemplo, seriam muito irritantes para implementar com objetos não nulos.
Karl Bielefeldt

2
Você não precisa verificar com o tipo Maybe. O tipo Talvez é uma mônada por isso deve ter as funções map :: Maybe a -> (a -> b) e bind :: Maybe a -> (a -> Maybe b)definido sobre ele, para que possa continuar e linha mais cálculos para a maior parte com a reformulação utilizando uma instrução else if. E getValueOrDefault :: Maybe a -> (() -> a) -> apermite que você lide com o caso anulável. É muito mais elegante do que a correspondência de padrões Maybe aexplicitamente.
DetriusXii

1

Maybeé uma maneira muito funcional de pensar em um problema - existe uma coisa e ela pode ou não ter um valor definido. No sentido orientado a objetos, no entanto, substituímos essa idéia de uma coisa (não importa se ela tem ou não valor) por um objeto. Claramente, um objeto tem um valor. Caso contrário, dizemos que o objeto é null, mas o que realmente queremos dizer é que não há nenhum objeto. A referência que temos ao objeto não aponta para nada. Traduzir Maybepara um conceito de OO não faz nada de novo - na verdade, apenas cria um código muito mais confuso. Você ainda precisa ter uma referência nula para o valor doMaybe<T>. Você ainda precisa fazer verificações nulas (na verdade, é necessário fazer muito mais verificações nulas, sobrecarregando seu código), mesmo que agora elas sejam chamadas de "talvez verificações". Certamente, você escreverá um código mais robusto, como afirma o autor, mas eu diria que esse é o caso porque você tornou a linguagem muito mais abstrata e obtusa, exigindo um nível de trabalho desnecessário na maioria dos casos. De bom grado, eu aceitava uma NullReferenceException de vez em quando do que lidava com o código espaguete fazendo uma verificação Talvez sempre que eu acessava uma nova variável.


2
Eu acho que isso levaria a menos verificações nulas, porque você só precisa verificar se vê um Talvez <T> e não precisa se preocupar com o resto dos tipos.
aochagavia

1
@svick Maybe<T>tem que permitir nullcomo um valor, porque o valor pode não existir. Se eu tiver Maybe<MyClass>, e ele não tiver um valor, o campo value deverá conter uma referência nula. Não há mais nada que seja comprovadamente seguro.
mgw854

1
@ mgw854 Claro que existe. Na linguagem OO, Maybepoderia ser uma classe abstrata, com duas classes que herdam dela: Some(que possui um campo para o valor) e None(que não possui esse campo). Dessa forma, o valor nunca é null.
svick

6
Com "not-nullability" por padrão, e talvez <T>, você pode ter certeza de que algumas variáveis ​​sempre contêm objetos. Ou seja, todas as variáveis que não estão Talvez <T>
aochagavia

3
@ mgw854 O objetivo dessa alteração seria aumentar o poder expressivo do desenvolvedor. No momento, o desenvolvedor sempre deve assumir que a referência ou ponteiro pode ser nulo e precisa verificar para garantir que haja valor utilizável. Ao implementar essa alteração, você fornece ao desenvolvedor o poder de dizer que ele realmente precisa de um valor válido e solicita a verificação do compilador para garantir que o valor válido foi passado. Mas ainda dando a opção de desenvolvedor, aceitar e não ter um valor divulgado.
Euphoric

1

O conceito de nullpode ser facilmente rastreado até C, mas não é aí que está o problema.

Minha linguagem cotidiana de escolha é C # e eu continuaria nullcom uma diferença. O C # possui dois tipos de tipos, valores e referências . Os valores nunca podem ser null, mas há momentos em que eu gostaria de expressar que nenhum valor é perfeitamente adequado. Para fazer isso, o C # usa Nullabletipos, assim intcomo o valor e int?o valor anulável. É assim que acho que os tipos de referência também devem funcionar.

Veja também: Referência nula pode não ser um erro :

Referências nulas são úteis e algumas vezes indispensáveis ​​(considere a quantidade de problemas se você pode ou não retornar uma cadeia de caracteres em C ++). O erro realmente não está na existência dos ponteiros nulos, mas na maneira como o sistema de tipos os trata. Infelizmente, a maioria das linguagens (C ++, Java, C #) não as manipula corretamente.


0

Eu acho que isso ocorre porque a programação funcional está muito preocupada com tipos , especialmente tipos que combinam outros tipos (tuplas, funções como tipos de primeira classe, mônadas etc.) do que a programação orientada a objetos (ou pelo menos inicialmente).

Versões modernas das linguagens de programação das quais você está falando (C ++, C #, Java) são todas baseadas em linguagens que não tinham nenhuma forma de programação genérica (C, C # 1.0, Java 1). Sem isso, você ainda pode fazer algum tipo de diferença entre objetos anuláveis ​​e não anuláveis ​​na linguagem (como referências em C ++, que não podem ser null, mas também são limitadas), mas é muito menos natural.


Eu acho que no caso de programação funcional, é o caso do FP não ter tipos de referência ou ponteiro. No FP tudo é um valor. E se você tem um tipo de ponteiro, fica fácil dizer "ponteiro para nada".
Euphoric

0

Eu acho que a razão fundamental é que relativamente poucas verificações nulas são necessárias para tornar um programa "seguro" contra corrupção de dados. Se um programa tentar usar o conteúdo de um elemento da matriz ou outro local de armazenamento que supostamente foi gravado com uma referência válida, mas não foi, o melhor resultado é que uma exceção seja lançada. Idealmente, a exceção indicará exatamente onde o problema ocorreu, mas o que importa é que algum tipo de exceção seja lançada antes que a referência nula seja armazenada em algum lugar que possa causar corrupção de dados. A menos que um método armazene um objeto sem tentar usá-lo de alguma maneira, uma tentativa de usar um objeto constituirá, por si só, uma espécie de "verificação nula".

Se alguém quiser garantir que uma referência nula que apareça onde não deveria causar uma exceção específica diferente NullReferenceException, muitas vezes será necessário incluir verificações nulas em todo o lugar. Por outro lado, apenas garantir que alguma exceção ocorra antes que uma referência nula possa causar "danos" além de qualquer que já tenha sido feito geralmente requer relativamente poucos testes - o teste geralmente seria necessário apenas nos casos em que um objeto armazenaria um objeto. referência sem tentar usá-lo, e a referência nula sobrescreveria uma válida ou faria com que outro código interpretasse mal outros aspectos do estado do programa. Tais situações existem, mas não são tão comuns; a maioria das referências nulas acidentais será capturada muito rapidamentese alguém procura por eles ou não .


é difícil ler este post (parede de texto). Você se importaria de editá -lo em uma forma melhor?
Gnat

1
@gnat: Isso é melhor?
Supercat

0

" Maybe," como está escrito, é uma construção de nível superior a nula. Usando mais palavras para defini-lo, talvez seja "um ponteiro para uma coisa ou um ponteiro para nada, mas o compilador ainda não recebeu informações suficientes para determinar qual". Isso obriga a verificar explicitamente cada valor constantemente, a menos que você construa uma especificação de compilador suficientemente inteligente para acompanhar o código que você escreve.

Você pode fazer uma implementação de Maybe com uma linguagem que tenha nulos facilmente. C ++ possui um na forma de boost::optional<T>. Tornar o equivalente a null com Maybe é muito difícil. Em particular, se eu tiver um Maybe<Just<T>>, não posso atribuí-lo a nulo (porque esse conceito não existe), enquanto a T**em uma linguagem com nulo é muito fácil de atribuir a nulo. Isso força a pessoa a usar Maybe<Maybe<T>>, que é totalmente válida, mas forçará você a fazer muito mais verificações para usar esse objeto.

Algumas linguagens funcionais usam Talvez porque nulo exija comportamento indefinido ou manipulação de exceções, nenhum dos quais é um conceito fácil de mapear para sintaxes da linguagem funcional. Talvez preencha o papel muito melhor nessas situações funcionais, mas nas linguagens processuais, nulo é o rei. Não é uma questão de certo e errado, apenas uma questão do que torna mais fácil dizer ao computador para fazer o que você deseja.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.