Por que as referências nulas são evitadas enquanto o lançamento de exceções é considerado bom?

21

Eu não entendo muito bem o bashing consistente de referências nulas por algumas pessoas da linguagem de programação. O que há de tão ruim neles? Se eu solicitar acesso de leitura a um arquivo que não existe, ficarei perfeitamente feliz em receber uma exceção ou uma referência nula. No entanto, as exceções são consideradas boas, mas as referências nulas são consideradas ruins. Qual é o raciocínio por trás disso?

davidk01
fonte
2
Alguns idiomas falham em nulo melhor que outros. Para um "código gerenciado", como .Net / Java, uma referência nula é apenas outro tipo de problema, enquanto outro código nativo pode não lidar com isso da maneira mais fácil (você não mencionou um idioma específico). Mesmo em um mundo gerenciado, às vezes você deseja escrever um código à prova de falhas (incorporado ?, armas?) E, às vezes, deseja fazer uma reclamação em voz alta o mais rápido possível (teste de unidade). Ambos os tipos de código podem estar chamando para a mesma biblioteca - isso seria um problema. Em geral, acho que o código que tenta não prejudicar os sentimentos dos computadores é uma má idéia. A segurança contra falhas é DURA de qualquer maneira.
Job
@ Job: Isso está defendendo a preguiça. Se você souber como lidar com uma exceção, você lida com ela. Às vezes, esse manuseio pode envolver o lançamento de outra exceção, mas você nunca deve permitir que uma exceção de referência nula não seja tratada. Sempre. Esse é o pesadelo de todo programador de manutenção; é a exceção mais inútil em toda a árvore. Basta perguntar ao Stack Overflow .
Aaronaught
ou, em outras palavras, por que não retornar um código de algum tipo para representar informações de erro. Este argumento vai se enfurecer nos próximos anos.
Gbjbaanb

Respostas:

24

Referências nulas não são "evitadas" mais do que exceções, pelo menos por alguém que eu já conheci ou li. Eu acho que você está entendendo mal a sabedoria convencional.

O que é ruim é uma tentativa de acessar uma referência nula (ou desreferenciar um ponteiro nulo etc.). Isso é ruim porque sempre indica um bug; você nunca faria algo assim de propósito, e se estiver fazendo de propósito, isso é ainda pior, porque torna impossível distinguir o comportamento esperado do comportamento de buggy.

Existem certos grupos marginais por aí que realmente odeiam o conceito de nulidade por algum motivo, mas, como Ed aponta , se você não tiver nullou nilentão precisará substituí-lo por algo mais, o que pode levar a algo pior que uma falha (como corrupção de dados).

Muitas estruturas, de fato, abraçam os dois conceitos; por exemplo, no .NET, um padrão frequente que você verá é um par de métodos, um prefixado pela palavra Try(como TryGetValue). No Trycaso, a referência é configurada para seu valor padrão (geralmente null) e, no outro caso, uma exceção é lançada. Não há nada errado com nenhuma das abordagens; ambos são usados ​​com freqüência em ambientes que os suportam.

Realmente tudo depende da semântica. Se nullfor um valor de retorno válido, como no caso geral de pesquisar uma coleção, retorne null. Por outro lado, se é não um valor de retorno válido - por exemplo, procurar um registro usando uma chave primária que veio de seu próprio banco de dados - então retornando nullseria uma má idéia, porque o chamador não será esperando por isso e, provavelmente, não vai verificar isso.

É realmente muito simples descobrir qual semântica usar: faz algum sentido que o resultado de uma função seja indefinido? Nesse caso, você pode retornar uma referência nula. Caso contrário, lance uma exceção.

Aaronaught
fonte
5
Na verdade, existem idiomas que não têm nulo ou nulo e não "precisam substituí-lo por outra coisa". Referências anuláveis ​​sugerem que pode haver algo lá ou pode não haver. Se você simplesmente exigir que o usuário verifique explicitamente se há algo lá, você resolveu o problema. Veja haskell para um exemplo da vida real.
precisa
3
@ErikKronberg, sim, o "erro de bilhão de dólares" e toda essa bobagem, o desfile de pessoas fazendo isso e alegando que é novo e fascinante nunca acaba, é por isso que o tópico anterior foi excluído. Essas substituições revolucionárias que as pessoas nunca deixam de mencionar são sempre uma variante do objeto nulo, opção ou contrato, que na verdade não elimina magicamente o erro lógico subjacente, apenas o adia ou promove, respectivamente. De qualquer forma, esta é obviamente uma pergunta sobre línguas que a programação fazer têm null, por isso, realmente, Haskell é bastante irrelevante aqui.
precisa saber é o seguinte
1
você está argumentando seriamente que não exigir um teste para nulo é tão bom quanto exigi-lo?
Johanna Larsson
3
@ErikKronberg: Sim, estou "argumentando seriamente" que ter que testar nulo não é particularmente diferente de (a) ter que projetar todas as camadas de um aplicativo em torno do comportamento de um Objeto Nulo, (b) ter que corresponder ao padrão Opções o tempo todo ou (c) não permitir que o destinatário diga "eu não sei" e forçar uma exceção ou falha. Há uma razão pela qual o nulldesempenho persiste tão bem por tanto tempo, e a maioria das pessoas que dizem o contrário parece ser acadêmica com pouca experiência no design de aplicativos do mundo real com restrições como requisitos incompletos ou consistência eventual.
precisa saber é o seguinte
3
@Aaronaught: Às vezes eu gostaria que houvesse um botão de voto negativo para comentários. Não há razão para reclamar assim.
Michael Shaw
11

A grande diferença é que, se você deixar de fora o código para manipular NULLs, seu código continuará possivelmente travando posteriormente com alguma mensagem de erro não relacionada, onde, como nas exceções, a exceção será gerada no ponto inicial da falha (abertura um arquivo para leitura no seu exemplo).

John Gaines Jr.
fonte
4
Falha ao lidar com um NULL seria uma ignorância intencional da interface entre o seu código e o que o devolveu. Os desenvolvedores que cometem esse erro cometem outros em um idioma que não executa NULLs.
Blrfl 23/05
@Blrfl, idealmente, os métodos são bastante curtos e, portanto, é fácil descobrir exatamente onde o problema está acontecendo. Um bom depurador geralmente pode capturar bem uma exceção de referência nula, mesmo que o código seja longo. O que devo fazer se estiver tentando ler uma configuração crítica de um registro, que não existe? Meu registro está bagunçado e é melhor que eu falhe e irrite o usuário do que recriar silenciosamente um nó e configurá-lo como padrão. E se um vírus fizesse isso? Então, se eu receber um nulo, devo lançar uma exceção especializada ou simplesmente deixá-la rasgar? Com métodos curtos, qual é a grande diferença?
Job
@ Job: E se você não tiver um depurador? Você percebe que 99,99% do tempo seu aplicativo será executado em um ambiente de release? Quando isso acontecer, você desejará ter usado uma exceção mais significativa. Talvez seu aplicativo tenha que falhar e incomodar o usuário, mas pelo menos ele produzirá informações de depuração que permitirão rastrear prontamente o problema, mantendo assim o mínimo aborrecimento.
Aaronaught
@ Birfl, às vezes não quero lidar com o caso em que um nulo seria natural para retornar. Por exemplo, suponha que eu tenha chaves de mapeamento de contêiner para valores. Se minha lógica garantir que eu nunca tente ler valores que não armazenei primeiro, nunca retornarei nulo. Nesse caso, prefiro ter uma exceção que forneça o máximo de informações possível para indicar o que deu errado, em vez de retornar um nulo para falhar misteriosamente em outro lugar do programa.
Winston Ewert 23/05
Dito de outra maneira, com exceções, tenho que lidar explicitamente com o caso incomum ou meu programa morre imediatamente. Com referências nulas, se o código não tratar explicitamente o caso incomum, ele tentará mancar. Acho melhor aceitar o caso de falha agora.
Winston Ewert
8

Como os valores nulos não são uma parte necessária de uma linguagem de programação e são uma fonte consistente de erros. Como você diz, abrir um arquivo pode resultar em falha, que pode ser comunicada novamente como um valor de retorno nulo ou por meio de uma exceção. Se valores nulos não forem permitidos, existe uma maneira consistente e singular de comunicar a falha.

Além disso, esse não é o problema mais comum com nulos. A maioria das pessoas se lembra de procurar nulo depois de chamar uma função que pode retorná-la. O problema surge muito mais em seu próprio design, permitindo que variáveis ​​sejam nulas em vários pontos na execução do seu programa. Você pode criar seu código de forma que valores nulos nunca sejam permitidos, mas se nulo não fosse permitido no nível do idioma, nada disso seria necessário.

No entanto, na prática, você ainda precisaria de alguma maneira para indicar se uma variável é ou não inicializada. Você teria uma forma de erro em que seu programa não falha, mas continua usando algum valor padrão possivelmente inválido. Sinceramente, não sei o que é melhor. Pelo meu dinheiro, gosto de bater cedo e com frequência.

Ed S.
fonte
Considere uma subclasse não inicializada com uma sequência de identificação e todos os seus métodos geram exceções. Se um deles aparecer, você sabe o que aconteceu. Em sistemas incorporados com memória limitada, a versão de produção do Uninitialized factory pode retornar nulo.
Jim Balter
3
@ Jim Balter: Acho que estou confuso sobre como isso realmente ajudaria na prática. Em qualquer programa não trivial, você terá que, em algum momento, lidar com valores que podem não ser inicializados. Portanto, deve haver alguma maneira de significar um valor padrão. Como tal, você ainda precisa verificar isso antes de continuar. Portanto, em vez de uma possível falha, agora você está trabalhando potencialmente com dados inválidos.
Ed S.
1
Você também sabe o que aconteceu quando obtém nullcomo valor de retorno: as informações solicitadas não existem. Não há diferença. De qualquer forma, o chamador deve validar o valor de retorno se realmente precisar usar essas informações para algum propósito específico. Seja validar a null, um objeto nulo ou uma mônada, não faz diferença prática.
Aaronaught 23/05
1
@ Jim Balter: Certo, ainda não vejo a diferença prática e não vejo como isso facilita a vida de pessoas que escrevem programas reais fora da academia. Não significa que não há benefícios, simplesmente que eles não me parecem evidentes.
Ed S.
1
Eu expliquei a diferença prática duas vezes com uma classe Não inicializada - identifica a origem do item nulo - possibilitando identificar o erro quando ocorre uma desreferência de nulo. Quanto às linguagens projetadas em torno de paradigmas sem nulo, elas evitam o problema desde o início - fornecem maneiras alternativas de programar. Que evitam variáveis ​​não inicializadas; isso pode parecer difícil de entender se você não estiver familiarizado com eles. A programação estruturada, que também evita uma grande classe de bugs, também já foi considerada "acadêmica".
Jim Balter
5

Tony Hoare, que criou a idéia de uma referência nula em primeiro lugar, chama de erro de um milhão de dólares .

O problema não é sobre referências nulas em si, mas sobre a falta de verificação de tipo adequada na maioria dos idiomas seguros (de outro modo).

Essa falta de suporte, no idioma, significa que os bugs "null-bugs" podem estar ocultos no programa por um longo tempo antes de serem detectados. Essa é a natureza dos bugs, é claro, mas os "bugs nulos" agora são conhecidos por serem evitáveis.

Esse problema está especialmente presente em C ou C ++ (por exemplo) devido ao erro "rígido" que ele causa (uma falha do programa, imediata, sem recuperação elegante).

Em outros idiomas, sempre há a questão de como lidar com eles.

Em Java ou C #, você obteria uma exceção se tentar invocar um método em uma referência nula, e isso pode ser bom. E, portanto, a maioria dos programadores de Java ou C # está acostumada a isso e não entende por que alguém gostaria de fazer o contrário (e rindo do C ++).

No Haskell, você deve fornecer explicitamente uma ação para o caso nulo e, portanto, os programadores do Haskell se gabam de seus colegas, porque eles acertaram (certo?).

É, na verdade, o antigo debate sobre código de erro / exceção, mas desta vez com um valor do Sentinel em vez de código de erro.

Como sempre, o que for mais apropriado realmente depende da situação e da semântica que você está procurando.

Matthieu M.
fonte
Precisamente. nullé realmente mau, porque subverte o sistema de tipos . (Concedido, a alternativa tem uma desvantagem em sua verbosidade, pelo menos em alguns idiomas.)
Mecânica caracol
2

Você não pode anexar uma mensagem de erro legível por humanos a um ponteiro nulo.

(Você pode, no entanto, deixar uma mensagem de erro em um arquivo de log.)

Em alguns idiomas / ambientes que permitem aritmética do ponteiro, se um dos argumentos do ponteiro for nulo e for permitido no cálculo, o resultado seria um ponteiro inválido e não nulo . (*) Mais poder para você.

(*) Isso acontece muito na programação COM , onde se você tentar chamar um método de interface, mas o ponteiro da interface for nulo, isso resultaria em uma chamada para um endereço inválido que é quase, mas não exatamente, exatamente diferente de zero.

rwong
fonte
2

Retornar NULL (ou zero numérico ou falso booleano) para sinalizar um erro está errado técnica e conceitualmente.

Tecnicamente, você está sobrecarregando o programador com a verificação do valor de retorno imediata do , no ponto exato em que ele é retornado. Se você abrir vinte arquivos em uma linha e a sinalização de erro for feita retornando NULL, o código de consumo deverá verificar cada arquivo lido individualmente e interromper quaisquer loops e construções semelhantes. Esta é uma receita perfeita para código desordenado. Se, no entanto, você optar por sinalizar o erro lançando uma exceção, o código consumidor poderá optar por manipular a exceção imediatamente ou deixá-la subir tantos níveis quanto apropriado, mesmo em chamadas de função. Isso cria um código muito mais limpo.

Conceitualmente, se você abrir um arquivo e algo der errado, retornar um valor (mesmo NULL) estará errado. Você não tem nada para retornar, porque sua operação não foi concluída. Retornar NULL é o equivalente conceitual de "Eu li com sucesso o arquivo e aqui está o que ele contém - nada". Se é isso que você deseja expressar (ou seja, se NULL faz sentido como um resultado real para a operação em questão), então retorne NULL, por todos os meios, mas se desejar sinalizar um erro, use exceções.

Historicamente, os erros eram relatados dessa maneira, porque linguagens de programação como C não possuem manipulação de exceção incorporada à linguagem, e a maneira recomendada (usando saltos longos) é um pouco cabeluda e meio contra-intuitiva.

Há também um lado da manutenção no problema: com exceções, você precisa escrever um código extra para lidar com a falha; caso contrário, o programa falhará cedo e com dificuldade (o que é bom). Se você retornar NULL para sinalizar erros, o comportamento padrão do programa é ignorar o erro e continuar, até que isso acarrete outros problemas futuros - dados corrompidos, segfaults, NullReferenceExceptions, dependendo do idioma. Para sinalizar o erro com antecedência e em voz alta, você precisa escrever um código extra e adivinhar: essa é a parte que fica de fora quando você tem um prazo apertado.

tdammers
fonte
1

Como já apontado, muitos idiomas não convertem uma desreferenciação de um ponteiro nulo em uma exceção capturável. Fazer isso é um truque relativamente moderno. Quando o problema do ponteiro nulo foi reconhecido pela primeira vez, as exceções ainda não haviam sido inventadas.

Se você permitir ponteiros nulos como um caso válido, esse é um caso especial. Você precisa de lógica de manipulação de casos especiais, geralmente em muitos lugares diferentes. Isso é complexidade extra.

Seja relacionado a ponteiros potencialmente nulos ou não, se você não usa lançamentos de exceção para lidar com casos excepcionais, deve tratar esses casos excepcionais de outra maneira. Normalmente, toda chamada de função deve ter verificações para esses casos excepcionais, para impedir que a chamada de função seja chamada de forma inadequada ou para detectar o caso de falha quando a função sair. Essa complexidade extra pode ser evitada usando exceções.

Mais complexidade geralmente significa mais erros.

Alternativas ao uso de ponteiros nulos nas estruturas de dados (por exemplo, para marcar o início / fim de uma lista vinculada) incluem o uso de itens sentinela. Eles podem fornecer a mesma funcionalidade com muito menos complexidade. No entanto, pode haver outras maneiras de gerenciar a complexidade. Uma maneira é agrupar o ponteiro potencialmente nulo em uma classe de ponteiro inteligente, para que as verificações nulas sejam necessárias apenas em um local.

O que fazer com o ponteiro nulo quando detectado? Se você não conseguir criar um tratamento de caso excepcional, sempre poderá lançar uma exceção e delegar efetivamente esse tratamento de caso especial ao chamador. E é exatamente isso que alguns idiomas fazem por padrão quando você desrefere um ponteiro nulo.

Steve314
fonte
1

Específico para C ++, mas as referências nulas são evitadas porque a semântica nula no C ++ está associada aos tipos de ponteiro. É bastante razoável que uma função de arquivo aberto falhe e retorne um ponteiro nulo; de fato, a fopen()função faz exatamente isso.

MSalters
fonte
De fato, se você se deparar com uma referência nula em C ++, é porque seu programa está quebrado .
Kaz Dragon
1

Depende do idioma.

Por exemplo, o Objective-C permite enviar uma mensagem para um objeto nulo (nulo) sem problemas. Uma chamada para nil retorna nulo também e é considerada um recurso de idioma.

Eu pessoalmente gosto, pois você pode confiar nesse comportamento e evitar todas essas if(obj == null)construções aninhadas complicadas .

Por exemplo:

if (myObject != nil && [myObject doSomething])
{
    ...
}

Pode ser reduzido para:

if ([myObject doSomething])
{
    ...
}

Em resumo, torna seu código mais legível.

Martin Wickman
fonte
0

Eu não dou a mínima se você retorna um nulo ou lança uma exceção, desde que você o documente.

SnoopDougieDoug
fonte
E também nomeie : GetAddressMaybeou GetSpouseNameOrNull.
rwong 24/05
-1

Uma referência nula geralmente ocorre porque alguma coisa na lógica do programa foi perdida, ou seja: você chegou a uma linha de código sem passar pela configuração necessária para esse bloco de código.

Por outro lado, se você lançar uma exceção para algo, significa que reconheceu que uma situação específica pode ocorrer na operação normal do programa e está sendo tratada.

Dave
fonte
2
Uma referência nula pode "ocorrer" por um grande número de razões, muito poucas delas pertencentes a algo que está faltando. Talvez você também esteja confundindo com uma exceção de referência nula .
Aaronaught 23/05
-1

Referências nulas geralmente são muito úteis: por exemplo, um elemento em uma lista vinculada pode ter algum sucessor ou nenhum sucessor. Usar nullpara "nenhum sucessor" é perfeitamente natural. Ou então, uma pessoa pode ter um cônjuge ou não - usar nullpara "pessoa que não tem cônjuge" é perfeitamente natural, muito mais natural do que ter algum "não tem cônjuge" - valor especial ao qual o Person.Spousemembro poderia se referir.

Mas: muitos valores não são opcionais. Em um programa típico de POO, eu diria que mais da metade das referências não pode ser nullposterior à inicialização ou o programa falhará. Caso contrário, o código teria que estar cheio de if (x != null)cheques. Então, por que toda referência deve ser anulável por padrão? Realmente deveria ser o contrário: as variáveis ​​devem ser não anuláveis ​​por padrão, e você deve explicitamente dizer "oh, e esse valor também pode ser null".

nikie
fonte
há alguma coisa nesta discussão que você gostaria de adicionar à sua resposta? Esse tópico de comentário esquentou um pouco e gostaríamos de limpar isso. Qualquer discussão estendida deve ser feita para conversar .
No meio de toda essa discussão, poderia ter havido um ou dois pontos de interesse real. Infelizmente, não vi o comentário de Mark até ter podado a discussão prolongada. No futuro, sinalize sua resposta para chamar a atenção dos moderadores, se você quiser preservar os comentários até ter tempo para analisá-los e editar sua resposta adequadamente.
Josh K
-1

Sua pergunta está confusa. Você quer dizer uma exceção de referência nula (que é realmente causada por uma tentativa de dereferência nula)? O motivo claro para não querer esse tipo de exceção é que ela não fornece informações sobre o que deu errado ou mesmo quando - o valor poderia ter sido definido como nulo em qualquer ponto do programa. Você escreve "Se eu solicitar acesso de leitura a um arquivo que não existe, ficarei perfeitamente feliz em receber uma exceção ou uma referência nula" - mas você não deverá estar perfeitamente feliz em obter algo que não indique a causa . Em nenhum lugar da cadeia "foi feita uma tentativa de remover a referência nula", há alguma menção à leitura ou a arquivos não existentes. Talvez você queira dizer que seria perfeitamente feliz em obter nulo como valor de retorno de uma chamada de leitura - isso é um assunto bem diferente, mas ainda não fornece informações sobre o motivo pelo qual a leitura falhou; isto'

Jim Balter
fonte
Não, não quero dizer uma exceção de referência nula. Em todas as línguas que conheço variáveis ​​não inicializadas, mas declaradas, são alguma forma de nulo.
Davidk01 23/05
Mas sua pergunta não era sobre variáveis ​​não inicializadas. E existem idiomas que não usam nulo para variáveis ​​não inicializadas, mas usam objetos wrapper que podem opcionalmente conter um valor - Scala e Haskell, por exemplo.
Jim Balter 23/05
1
"... mas use objetos wrapper que podem opcionalmente conter um valor ." O que claramente não é nada como um tipo anulável .
Aaronaught 24/05
1
No haskell, não existe uma variável não inicializada. Você pode potencialmente declarar um IORef e semear com None como um valor inicial, mas esse é o análogo de declarar uma variável em algum outro idioma e deixá-lo não inicializado, o que traz os mesmos problemas. Trabalhando no núcleo puramente funcional fora dos IO monad haskell, os programadores não recorrem a tipos de referência, portanto, não há problema de referência nula.
Davidk01 24/05
1
Se você tiver um valor "ausente" excepcional, é exatamente o mesmo que um valor "nulo" excepcional - se você tiver as mesmas ferramentas disponíveis para lidar com isso. Esse é um "se" significativo. Você precisa de complexidade extra para lidar com esse caso de qualquer maneira. Em Haskell, a correspondência de padrões e o sistema de tipos fornece uma maneira de ajudar a gerenciar essa complexidade. No entanto, existem outras ferramentas para gerenciar a complexidade em outros idiomas. Exceções são uma dessas ferramentas.
31311 Steve11