Deve-se verificar se null não espera nulo?

106

Na semana passada, tivemos uma discussão acalorada sobre o tratamento de nulos na camada de serviço do nosso aplicativo. A questão está no contexto .NET, mas será a mesma em Java e em muitas outras tecnologias.

A pergunta é: você deve sempre verificar se há nulos e fazer seu código funcionar, não importando o quê, ou deixar que uma exceção se eleve quando um nulo é recebido inesperadamente?

De um lado, a verificação de nulo onde você não está esperando (isto é, não possui uma interface de usuário para lidar com isso) é, na minha opinião, o mesmo que escrever um bloco try com captura vazia. Você está apenas escondendo um erro. O erro pode ser que algo mudou no código e null é agora um valor esperado, ou há algum outro erro e o ID incorreto é passado para o método.

Por outro lado, a verificação de nulos pode ser um bom hábito em geral. Além disso, se houver uma verificação, o aplicativo pode continuar funcionando, com apenas uma pequena parte da funcionalidade sem nenhum efeito. Em seguida, o cliente pode relatar um pequeno bug como "não é possível excluir o comentário", em vez de um bug muito mais grave, como "não é possível abrir a página X".

Qual prática você segue e quais são seus argumentos a favor ou contra uma ou outra abordagem?

Atualização:

Eu quero adicionar alguns detalhes sobre o nosso caso particular. Nós estávamos recuperando alguns objetos do banco de dados e fizemos algum processamento neles (digamos, construir uma coleção). O desenvolvedor que escreveu o código não previu que o objeto poderia ser nulo, portanto, ele não incluiu nenhuma verificação e, quando a página foi carregada, ocorreu um erro e a página inteira não foi carregada.

Obviamente, neste caso, deveria haver um cheque. Em seguida, entramos em uma discussão sobre se cada objeto processado deve ser verificado, mesmo que não seja esperado que esteja faltando, e se o eventual processamento deve ser interrompido silenciosamente.

O benefício hipotético seria que a página continuasse funcionando. Pense nos resultados de uma pesquisa no Stack Exchange em diferentes grupos (usuários, comentários, perguntas). O método poderia verificar se há nulo e abortar o processamento dos usuários (o que, devido a um erro, é nulo), mas retornar as seções "comentários" e "perguntas". A página continuaria funcionando, exceto que a seção "usuários" estaria faltando (o que é um bug). Devemos falhar cedo e quebrar a página inteira ou continuar a trabalhar e esperar que alguém perceba que a seção "usuários" está faltando?

    
por Stilgar 06.05.2012 / 17:42
fonte

16 respostas

97

A questão não é tanto se você deve verificar por null ou deixar o tempo de execução lançar uma exceção; é como você deve responder a uma situação tão inesperada.

Suas opções, então, são:

  • Lance uma exceção genérica ( NullReferenceException ) e deixe que ela se eleve; se você não fizer o null verificar você mesmo, isso é o que acontece automaticamente.
  • Lança uma exceção personalizada que descreve o problema em um nível superior; Isso pode ser alcançado lançando o null check ou pegando um NullReferenceException e lançando a exceção mais específica.
  • Recupere substituindo um valor padrão adequado.

Não há regra geral de qual é a melhor solução. Pessoalmente, eu diria:

  • A exceção genérica é melhor se a condição de erro for um sinal de um bug sério em seu código, ou seja, o valor que é nulo nunca deve ter permissão para chegar lá em primeiro lugar. Tal exceção pode, então, fazer todo o caminho até o registro de erros que você configurou, para que alguém seja notificado e corrija o erro.
  • A solução de valor padrão é boa se fornecer saída semi-útil for mais importante do que correção, como um navegador da web que aceite HTML tecnicamente incorreto e faça o melhor esforço para processá-lo de maneira sensata.
  • Caso contrário, eu iria com a exceção específica e lidaria com isso em algum lugar adequado.
por 06.05.2012 / 20:58
fonte
56

IMHO tentando lidar com valores nulos que você não espera levar a um código excessivamente complicado. Se você não espera nulo, deixe claro jogando ArgumentNullException . Eu fico realmente frustrado quando as pessoas verificam se o valor é nulo e tentam escrever algum código que não faz sentido. O mesmo se aplica ao uso de SingleOrDefault (ou pior ainda, a obtenção de coleções) quando alguém realmente espera Single e muitos outros casos em que as pessoas têm medo (não sei bem o quê) de afirmar claramente sua lógica.

    
por 06.05.2012 / 17:58
fonte
34

O hábito de verificar por null a minha experiência vem de desenvolvedores anteriores de C ou C ++; nesses idiomas você tem uma boa chance de ocultar um erro grave quando não está verificando NULL . Como você sabe, em Java ou C # as coisas são diferentes, usar uma referência nula sem verificar null causará uma exceção, portanto, o erro não será secretamente escondido, desde que você não o ignore no lado da chamada. Portanto, na maioria dos casos, a verificação explícita de null não faz muito sentido e complica demais o código mais do que o necessário. É por isso que eu não considero a verificação de nulos como um bom hábito nesses idiomas (pelo menos, não em geral).

Claro, há exceções dessa regra, eis as que posso pensar:

  • você deseja uma mensagem de erro melhor e legível, informando ao usuário em qual parte do programa ocorreu a referência nula incorreta. Portanto, seu teste para null lança apenas uma exceção diferente, com um texto de erro diferente. Obviamente, nenhum erro será mascarado dessa maneira.

  • você quer que seu programa "falhe mais cedo". Por exemplo, você deseja verificar um valor nulo de um parâmetro de construtor, onde a referência de objeto seria armazenada em uma variável de membro e usada posteriormente.

  • você pode lidar com a situação de um representante nulo com segurança, sem o risco de falhas subsequentes e sem mascarar um erro grave.

  • você deseja um tipo completamente diferente de sinalização de erro (por exemplo, retornando um código de erro) em seu contexto atual

por 06.05.2012 / 18:12
fonte
15

Use para testar e indicar pré / pós-condições e invariantes para o seu código.

Isso torna muito mais fácil entender o que o código espera e o que ele pode esperar.

Uma declaração, IMO, é uma verificação adequada porque é:

  • eficiente (geralmente não usado na versão),
  • breve (geralmente uma única linha) e
  • clear (pré-condição violada, isso é um erro de programação).

Acho que o código de autodocumentação é importante, portanto, ter um cheque é bom. A programação defensiva e rápida faz com que a manutenção seja mais fácil, onde normalmente a maior parte do tempo é gasta.

Atualizar

Escreve sua elaboração. Geralmente é bom dar ao usuário final um aplicativo que funcione bem em bugs, ou seja, mostra o máximo possível. A robustez é boa, porque o céu só sabe o que vai quebrar em um momento crítico.

No entanto, os desenvolvedores & Os testadores devem estar cientes de erros o mais rápido possível, então você provavelmente quer uma estrutura de registro com um gancho que possa exibir um alerta para o usuário de que houve um problema. O alerta provavelmente deve ser mostrado para todos os usuários, mas provavelmente pode ser adaptado de forma diferente dependendo do ambiente de tempo de execução.

    
por 06.05.2012 / 21:51
fonte
7

Falha no início, falha com frequência. null é um dos melhores valores inesperados que você pode obter, porque você pode falhar rapidamente assim que tentar usá-lo. Outros valores inesperados não são tão fáceis de detectar. Como null falha automaticamente quando você tenta usá-lo, eu diria que ele não exige uma verificação explícita.

    
por 06.05.2012 / 17:57
fonte
6
  • A verificação de um nulo deve ser sua segunda natureza

  • Sua API será usada por outras pessoas e a expectativa delas provavelmente será diferente da sua

  • Testes unitários completos normalmente destacam a necessidade de uma verificação de referência nula

  • A verificação de nulos não implica declarações condicionais. Se você se preocupa que a verificação de nulos torne seu código menos legível, convém considerar contratos de código .NET.

  • Considere instalar o ReSharper (ou similar) para procurar em seu código por verificações de referência nula ausentes

por 06.05.2012 / 21:47
fonte
4

Eu consideraria a questão do ponto de vista da semântica, ou seja, perguntar-me o que representa um ponteiro NULL ou um argumento de referência nula.

Se uma função ou método foo () tiver um argumento x do tipo T, x pode ser obrigatório ou opcional .

Em C ++, você pode passar um argumento obrigatório como

  • Um valor, por exemplo void foo (T x)
  • Uma referência, por ex. void foo (T & x).

Em ambos os casos, você não tem o problema de verificar um ponteiro NULL. Portanto, em muitos casos, você não precisa de uma verificação de ponteiro nulo em todos os argumentos obrigatórios.

Se você está passando um argumento opcional , você pode usar um ponteiro e usar o valor NULL para indicar que nenhum valor foi dado:

void foo(T* x)

Uma alternativa é usar um ponteiro inteligente como

void foo(shared_ptr<T> x)

Neste caso, você provavelmente desejará verificar o ponteiro no código, porque faz diferença para o método foo () se x contém um valor ou nenhum valor.

O terceiro e último caso é que você usa um ponteiro para uma obrigatória argumento. Neste caso, chamar foo (x) com x == NULL é um erro e você tem que decidir como lidar com isso (o mesmo que com um índice fora dos limites ou qualquer entrada inválida):

  1. Sem verificação: se houver um bug, deixe-o travar e espere que esse bug apareça cedo o suficiente (durante o teste). Se isso não acontecer (se você falhar em falhar o suficiente), espere que ele não apareça em um sistema de produção porque a experiência do usuário é que eles vêem a falha do aplicativo.
  2. Verifique todos os argumentos no início do método e relate argumentos NULL inválidos, por exemplo, lance uma exceção de foo () e deixe algum outro método lidar com isso. Provavelmente, a melhor coisa a fazer é mostrar ao usuário uma janela de diálogo dizendo que o aplicativo encontrou um erro interno e será fechado.

Além de uma maneira mais agradável de falhar (dando ao usuário mais informações), uma vantagem da segunda abordagem é que é mais provável que o bug seja encontrado: o programa falhará toda vez que o método for chamado com o erro argumentos, enquanto que com a abordagem 1, o erro só aparecerá se uma declaração que desreferencia o ponteiro for executada (por exemplo, se a ramificação direita de uma instrução if for inserida). Então, a abordagem 2 oferece uma forma mais strong de "falhar cedo".

Em Java você tem menos opções porque todos os objetos são passados usando referências que sempre podem ser nulas. Novamente, se o valor null representar um valor NONE de um argumento opcional, a verificação do valor nulo provavelmente fará parte da lógica de implementação.

Se o nulo é uma entrada inválida, então você tem um erro e eu aplicaria considerações semelhantes como no caso dos ponteiros C ++. Como em Java você pode pegar exceções de ponteiro nulo, a abordagem 1 acima é equivalente a abordagem 2 se o método foo () desreferenciar todos os argumentos de entrada em todos os caminhos de execução (o que provavelmente ocorre com muita freqüência para métodos pequenos).

Resumindo

  • Tanto em C ++ quanto em Java, verificar se argumentos opcionais são nulos faz parte da lógica do programa.
  • Em C ++, eu sempre verifico argumentos obrigatórios para garantir que uma exceção apropriada seja lançada para que o programa possa lidar com isso de maneira robusta.
  • Em Java (ou C #), eu verificaria argumentos obrigatórios se houver caminhos de execução que não serão lançados para ter uma forma mais strong de "falhar cedo".

EDITAR

Obrigado ao Stilgar para mais detalhes.

No seu caso, parece que você tem null como resultado de um método. Mais uma vez, acho que você deve primeiro esclarecer (e corrigir) a semântica de seus métodos antes você pode tomar uma decisão.

Então, você tem o método m1 () chamando o método m2 () e m2 () retorna uma referência (em Java) ou um ponteiro (em C ++) para algum objeto.

Qual é a semântica de m2 ()? Deve m2 () sempre retornar um resultado não nulo? Se este for o caso, então um resultado nulo é um erro interno (em m2 ()). Se você verificar o valor de retorno em m1 (), poderá ter um tratamento de erros mais robusto. Se você não fizer isso, provavelmente terá uma exceção, mais cedo ou mais tarde. Talvez não. Espero que a exceção seja lançada durante o teste e não após a implantação. Pergunta : se m2 () nunca deve retornar null, por que ele está retornando null? Talvez devesse lançar uma exceção em vez disso? m2 () é provavelmente buggy.

A alternativa é que retornar null faz parte da semântica do método m2 (), ou seja, possui um resultado opcional, que deve ser documentado na documentação Javadoc do método. Nesse caso, é OK ter um valor nulo. O método m1 () deve verificá-lo como qualquer outro valor: não há erro aqui, mas provavelmente verificar se o resultado é nulo é apenas parte da lógica do programa.

    
por 06.05.2012 / 19:51
fonte
3

Minha abordagem geral é nunca testar uma condição de erro que você não sabe como lidar com . Em seguida, a pergunta que precisa ser respondida em cada instância específica se torna: você pode fazer algo razoável na presença do valor null inesperado?

Se você puder continuar com segurança substituindo outro valor (uma coleção vazia, por exemplo, ou um valor ou instância padrão), faça isso por todos os meios. @ O exemplo de Shahbaz de World of Warcraft de substituir uma imagem por outra se enquadra nessa categoria; a própria imagem é provavelmente apenas decoração e não tem impacto funcional . (Em uma compilação de depuração, usaria uma cor gritando para chamar a atenção para o fato de que a substituição ocorreu, mas isso depende muito da natureza do aplicativo.) Detalhes do log sobre o erro, entretanto, para que você saiba que está ocorrendo; caso contrário, você estará escondendo um erro que provavelmente desejará saber. Escondê-lo do usuário é uma coisa (novamente, supondo que o processamento possa continuar com segurança); escondê-lo do desenvolvedor é bem diferente.

Se você não puder continuar com segurança na presença de um valor nulo, falhe com um erro sensato; em geral, prefiro falhar o mais cedo possível quando é óbvio que a operação não pode ser bem-sucedida, o que implica a verificação de condições prévias. Há momentos em que você não quer falhar imediatamente, mas adiar o fracasso pelo maior tempo possível; Essa consideração surge, por exemplo, em aplicativos relacionados à criptografia, nos quais os ataques de canal lateral poderiam divulgar informações que você não deseja conhecer. No final, deixe o erro chegar a algum manipulador de erro geral, que, por sua vez, registra detalhes relevantes e exibe uma mensagem de erro legal (por mais agradável que seja) ao usuário.

Também é importante notar que a resposta apropriada pode muito bem diferir entre compilações de testes / QA / debug, e compilações de produção / release. Nas compilações de depuração, eu iria por falhas prematuras e difíceis com mensagens de erro muito detalhadas, para tornar completamente óbvio que há um problema e permitir que um post-mortem determine o que precisa ser feito para corrigi-lo. As compilações de produção, quando confrontadas com erros recuperáveis com segurança , provavelmente devem favorecer a recuperação.

    
por 07.05.2012 / 13:31
fonte
2

Com certeza NULL não é esperado , mas sempre há bugs!

Acredito que você deve verificar se há NULL se não espera. Deixe-me dar exemplos (embora não estejam em Java).

  • Em World of Warcraft, devido a um bug, a imagem de um dos desenvolvedores apareceu em um cubo para muitos objetos. Imagine se o mecanismo de gráficos não tivesse esperado NULL ponteiros, teria havido uma falha, enquanto isso foi transformado em uma experiência não tão fatal (até mesmo engraçada) para o usuário. O mínimo é que você não teria inesperadamente perdido sua conexão ou qualquer um dos seus pontos de salvamento.

    Este é um exemplo em que a verificação de NULL impediu uma falha e o caso foi tratado silenciosamente.

  • Imagine um programa de caixa de banco. A interface faz algumas verificações e envia dados de entrada para uma parte subjacente para executar as transferências de dinheiro. Se a parte principal espera que não receba NULL , então um bug na interface pode causar um problema na transação, possivelmente algum dinheiro no meio se perdendo (ou pior, duplicado).

    Por "um problema" quero dizer uma falha (como no travamento crash (digamos que o programa está escrito em C ++), não uma exceção de ponteiro nulo). Nesse caso, a transação precisa ser revertida se NULL for encontrado (o que obviamente não seria possível se você não verificasse as variáveis contra NULL!)

Tenho certeza de que você entendeu a ideia.

Verificar a validade dos argumentos para as suas funções não sobrecarrega o seu código, pois é um casal que verifica no topo da sua função (não se espalha no meio) e aumenta grandemente a robustez.

Se você tiver que escolher entre "o programa informa ao usuário que ele falhou e normalmente desliga ou reverte para um estado consistente possivelmente criando um log de erros" e "Violação de acesso", você não escolheria o primeiro? ? Isso significa que cada função deve estar preparada para ser chamada por um código com bugs, em vez de esperar que tudo esteja correto, simplesmente porque os bugs sempre existem.

    
por 07.05.2012 / 02:00
fonte
2

Como as outras respostas apontaram Se você não espera NULL , deixe claro jogando ArgumentNullException . Na minha opinião, quando você está depurando o projeto ajuda você a descobrir as falhas na lógica do seu programa mais cedo.

Então você vai lançar o seu software, se você restaurar esses NullRefrences cheques, você não perder nada na lógica do seu software, ele só tem certeza que um bug grave não irá aparecer. / p>

Outro ponto é que, se a verificação NULL estiver nos parâmetros de uma função, você poderá decidir se a função é o local certo para fazê-la ou não; caso contrário, encontre todas as referências à função e, em seguida, antes de passar um parâmetro para a função, analise os cenários prováveis que podem levar a uma referência nula.

    
por 02.02.2015 / 19:40
fonte
1

Todo null no seu sistema é uma bomba-relógio esperando para ser descoberta. Verifique se há null nos limites em que seu código interage com o código que você não controla e proíba null em seu próprio código. Isso livra você do problema sem confiar ingenuamente no código externo. Use exceções ou algum tipo de Maybe / Nothing type.

    
por 29.01.2014 / 21:33
fonte
0

Enquanto uma exceção de ponteiro nulo eventualmente será lançada, ela será lançada longe do ponto onde você obteve o ponteiro nulo. Faça um favor a si mesmo e encurte o tempo que leva para consertar o bug registrando pelo menos o fato de você ter um ponteiro nulo o mais rápido possível.

Mesmo que você não saiba o que fazer agora, registrar o evento economizará seu tempo mais tarde. E talvez o cara que conseguir o ingresso consiga usar o tempo economizado para descobrir a resposta apropriada.

    
por 07.05.2012 / 15:15
fonte
-1

Como Fail early, fail fast como declarado por @DeadMG (@empi) não é possível porque o aplicativo da Web deve funcionar bem em bugs você deve aplicar a regra

nenhuma exceção capturada sem registro

para que você, como desenvolvedor, esteja ciente dos possíveis problemas, inspecionando os registros.

se você estiver usando uma estrutura de registro como log4net ou log4j, você pode configurar uma saída de registro especial adicional (também conhecida como appender) que envia erros para você e fatalerrors. então você fica informado .

[atualização]

se o usuário final da página não estiver ciente do erro, você pode configurar um anexador  que enviam erros e fatalistas que você fica informado.

se não houver problema em que o usuário final da página veja mensagens de erro crípticas, você poderá configurar um anexador que coloque o log de erros para o processamento de página no final da página.

Não importa como você usa o registro se você engolir a exceção, o programa continuará com dados que fazem sentido e o engolimento não é mais silencioso.

    
por 07.05.2012 / 09:38
fonte
-1

Já existem boas respostas para essa pergunta, pode ser uma adição a elas: prefiro asserções no meu código. Se o seu código puder funcionar apenas com objetos válidos, mas com nut com null, você deve declarar o valor nulo.

Afirmações nesse tipo de situação permitiriam que qualquer teste de unidade e / ou integração falhe com a mensagem exata, para que você possa resolver o problema rapidamente antes da produção. Se tal erro passar nos testes e for para a produção (onde as afirmações devem ser desativadas) você receberá o NpEx, e um bug severo, mas é melhor, do que um pequeno bug que é muito mais confuso e aparecendo em algum outro lugar código, e seria mais caro para corrigir.

Ainda mais se alguém precisar trabalhar com suas afirmações de código, ele lhe diz sobre suas suposições feitas durante a criação / gravação de seu código (neste caso: Ei, esse objeto deve ser apresentado!), o que facilita a manutenção. / p>     

por 11.05.2012 / 08:47
fonte
-1

Eu normalmente verifico nulos, mas "manipulo" nulos inesperados lançando uma exceção que forneça um pouco mais de detalhes sobre exatamente onde ocorreu o nulo.

Edit: Se você obtiver um nulo quando não espera que algo esteja errado - o programa deve morrer.

    
por 11.05.2012 / 19:53
fonte
-1

Depende da implementação da linguagem das exceções. As exceções permitem que você tome algumas medidas para evitar danos aos dados ou liberar recursos de forma limpa caso o sistema entre em um estado ou condição imprevisto. Isso é diferente do tratamento de erros, condição que você pode antecipar. Minha filosofia é que você deve ter o mínimo possível de manipulação de exceção. É barulho. Seu método pode fazer algo razoável ao lidar com a exceção? Pode se recuperar? Pode reparar ou evitar mais danos? Se você for pegar a exceção e depois retornar do método ou falhar de alguma outra forma inofensiva, então não há sentido em pegar a exceção. Você teria apenas adicionado ruído e complexidade ao seu método, e você pode estar escondendo a fonte de uma falha em seu projeto. Neste caso, eu deixaria a falha subir e ser capturada por um loop externo. Se você puder fazer algo como reparar um objeto ou marcá-lo como corrompido ou liberar algum recurso crítico, como um arquivo de bloqueio ou fechando um soquete de forma limpa, então este é um bom uso de exceções. Se você realmente espera que o NULL apareça freqüentemente como um caso de borda válido, então você deve lidar com isso no fluxo lógico normal com instruções if-the-else ou switch-case ou o que for, não com um tratamento de exceção. Por exemplo, um campo desabilitado em um formulário pode ser definido como NULL, que é diferente de uma string vazia representando um campo deixado em branco. Esta é uma área onde você deve usar o bom senso e bom senso. Eu nunca ouvi falar de boas regras sólidas para saber como lidar com esse problema em todas as situações.

Java falha em sua implementação de tratamento de exceções com "exceções verificadas", em que todas as exceções possíveis que podem ser levantadas por objetos usados em um método devem ser capturadas ou declaradas na cláusula "throws" do método. O é o oposto de C ++ e Python, onde você pode optar por manipular exceções como achar melhor. Em Java, se você modificar o corpo de um método para incluir uma chamada que pode gerar uma exceção, você terá que escolher adicionar manipulação explícita para uma exceção que talvez não se importe, adicionando ruído e complexidade ao seu código ou adicione essa exceção à cláusula "throws" do método que você está modificando, o que não apenas adiciona ruído e confusão, mas altera a assinatura do seu método. Você deve então ir a um código de modificação onde quer que seu método seja usado para manipular a nova exceção que seu método agora pode aumentar ou adicionar a exceção à cláusula "throws" desses métodos, acionando assim um efeito dominó de mudanças de código. O "Catch or Specify Requirement" deve ser um dos aspectos mais irritantes do Java. A exceção de exceção para RuntimeExceptions e a racionalização oficial de Java para este erro de design é fraca.

Esse último parágrafo teve pouco a ver com responder a sua pergunta. Eu odeio Java.

Noé

    
por 23.05.2015 / 16:18
fonte