Na semana passada, tivemos uma discussão acalorada sobre como lidar com nulos na camada de serviço de nosso aplicativo. A questão está no contexto do .NET, mas será a mesma em Java e em muitas outras tecnologias.
A pergunta era: você deve sempre verificar nulos e fazer com que seu código funcione, não importa o quê, ou deixar uma exceção surgir quando um nulo é recebido inesperadamente?
Por um lado, verificar nulo onde você não o espera (ou seja, não possui interface com o usuário para lidar com isso) é, na minha opinião, o mesmo que escrever um bloco try com catch vazio. Você está apenas ocultando um erro. O erro pode ser que algo mudou no código e nulo agora é um valor esperado, ou há algum outro erro e o ID errado é passado para o método.
Por outro lado, verificar nulos pode ser um bom hábito em geral. Além disso, se houver uma verificação, o aplicativo poderá continuar funcionando, com apenas uma pequena parte da funcionalidade sem 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".
Que prática você segue e quais são seus argumentos a favor ou contra qualquer uma dessas abordagens?
Atualizar:
Quero acrescentar alguns detalhes sobre o nosso caso em particular. Estávamos recuperando alguns objetos do banco de dados e fizemos algum processamento neles (digamos, construímos uma coleção). O desenvolvedor que escreveu o código não previu que o objeto pudesse ser nulo, portanto 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, nesse caso, deveria ter havido um cheque. Em seguida, discutimos se todos os objetos processados devem ser verificados, mesmo que não se espere que eles estejam ausentes, e se o processamento final 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 pode verificar se há nulo e abortar o processamento de usuários (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" estará ausente (o que é um bug). Devemos falhar cedo e quebrar a página inteira ou continuar a trabalhar e esperar que alguém note que a seção "usuários" está ausente?
fonte
assert(foo != null, "foo is web control within the repeater, there's no reason to expect it to be null, etc, etc...");
Respostas:
A questão não é tanto se você deve procurar
null
ou deixar o tempo de execução lançar uma exceção; é assim que você deve responder a uma situação tão inesperada.Suas opções, então, são:
NullReferenceException
) e deixe borbulhar; se você não fizer anull
verificação, é isso que acontece automaticamente.null
verificação ou capturandoNullReferenceException
e lançando a exceção mais específica.Não existe uma regra geral sobre qual é a melhor solução. Pessoalmente, eu diria:
fonte
IMHO tentar lidar com valores nulos que você não espera leva a códigos excessivamente complicados. Se você não espera nulo, deixe claro jogando
ArgumentNullException
. Fico realmente frustrado quando as pessoas verificam se o valor é nulo e, em seguida, tentam escrever um código que não faz sentido. O mesmo se aplica ao usoSingleOrDefault
(ou pior ainda, da coleta) quando alguém realmente esperaSingle
e em muitos outros casos em que as pessoas têm medo (eu realmente não sei o que) para afirmar claramente sua lógica.fonte
ArgumentNullException
, deve-se usar contratos de código (a menos que seja um código-fonte anterior a 2010 que não suporta contratos de código).ArgumentNullException
conta como "manipulando" o valor nulo: evita uma exceção de referência nula posterior.ArgumentNullException
deixa muito claro qual é o problema e como corrigi-lo. C # não costuma usar "indefinido". Se você chamar algo de uma maneira indefinida, a maneira como você está chamando não faz sentido. Uma exceção é a maneira correta de indicar isso. Agora, às vezes, tornarei métodos de análise que retornam nulos se oint
(por exemplo) não puder ser analisado. Mas esse é um caso em que um resultado incomparável é uma possibilidade legítima e não um erro de uso. Veja também este artigo .O hábito de
null
procurar a minha experiência vem de ex-desenvolvedores de C ou C ++; nesses idiomas, você tem uma boa chance de ocultar um erro grave ao não procurarNULL
. Como você sabe, em Java ou C # as coisas são diferentes, o uso de uma ref nula sem verificaçãonull
causará uma exceção; portanto, o erro não será oculto secretamente desde que você não o ignore no lado da chamada. Portanto, na maioria dos casos, procurar explicitamentenull
não faz muito sentido e complica demais o código mais do que o necessário. É por isso que não considero a verificação nula um bom hábito nesses idiomas (pelo menos, não em geral).Claro, existem exceções a essa regra, eis as que eu consigo pensar:
você deseja uma mensagem de erro legível por humanos, informando ao usuário em que 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 construtor, onde a referência do objeto seria armazenada apenas em uma variável de membro e usada posteriormente.
você pode lidar com a situação de uma ref nula com segurança, sem o risco de falhas subseqüentes 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) no seu contexto atual
fonte
Use asserts para testar e indicar pré / pós-condições e invariantes para seu código.
Torna muito mais fácil entender o que o código espera e com o que se espera que ele lide.
Uma declaração, IMO, é uma verificação adequada porque é:
Eu acho que o código de auto-documentação é importante, portanto, verificar é bom. A programação defensiva e à prova de falhas facilita a manutenção, que é onde geralmente passa mais tempo.
Atualizar
Escreveu sua elaboração. Geralmente, é bom oferecer ao usuário final um aplicativo que funcione bem com erros, ou seja, mostre o máximo possível. A robustez é boa, porque o céu sabe apenas o que quebrará em um momento crítico.
No entanto, os desenvolvedores e testadores devem estar cientes dos erros o mais rápido possível, portanto, você provavelmente deseja uma estrutura de log com um gancho que possa exibir um alerta ao usuário de que houve um problema. O alerta provavelmente deve ser mostrado a todos os usuários, mas provavelmente pode ser adaptado de maneira diferente, dependendo do ambiente de tempo de execução.
fonte
Falhe cedo, falhe frequentemente.
null
é um dos melhores valores inesperados que você pode obter, pois pode falhar rapidamente assim que tentar usá-lo. Outros valores inesperados não são tão fáceis de detectar. Comonull
falha automaticamente para você sempre que você tenta usá-lo, eu diria que ele não requer uma verificação explícita.fonte
A verificação de um nulo deve ser sua segunda natureza
Sua API será usada por outras pessoas e é provável que a expectativa delas seja diferente da sua
Testes de unidade detalhados normalmente destacam a necessidade de uma verificação de referência nula
A verificação de nulos não implica instruções condicionais. Se você se preocupa que a verificação nula torne seu código menos legível, considere os contratos de código .NET.
Considere instalar o ReSharper (ou similar) para pesquisar seu código por verificações de referência nula ausentes
fonte
Eu consideraria a questão do ponto de vista da semântica, ou seja, me perguntaria o que um ponteiro NULL ou um argumento de referência nulo representa.
Se uma função ou método foo () possui um argumento x do tipo T, x pode ser obrigatório ou opcional .
Em C ++, você pode passar um argumento obrigatório como
Nos dois casos, você não tem o problema de verificar um ponteiro NULL. Assim, em muitos casos, você não precisa de uma verificação de ponteiro nulo em tudo para argumentos obrigatórios.
Se você estiver passando um argumento opcional , poderá usar um ponteiro e usar o valor NULL para indicar que nenhum valor foi fornecido:
Uma alternativa é usar um ponteiro inteligente como
Nesse caso, você provavelmente deseja 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 um argumento obrigatório . Nesse caso, chamar foo (x) com x == NULL é um erro e você deve decidir como lidar com isso (o mesmo que com um índice fora dos limites ou qualquer entrada inválida):
Além de uma maneira mais agradável de falhar (fornecendo mais informações ao usuário), 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 argumentos errados, enquanto que com Abordagem 1, o bug só aparecerá se uma instrução que desreferenciar o ponteiro for executada (por exemplo, se o ramo direito de uma instrução if for inserido). Portanto, a abordagem 2 oferece uma forma mais forte de "falhar cedo".
Em Java, você tem menos opções porque todos os objetos são transmitidos usando referências que sempre podem ser nulas. Novamente, se o nulo representa um valor NENHUM de um argumento opcional, a verificação do nulo (provavelmente) fará parte da lógica de implementação.
Se o nulo é uma entrada inválida, você tem um erro e eu aplicaria considerações semelhantes às do ponteiro de C ++. Como em Java você pode capturar exceções de ponteiro nulo, a abordagem 1 acima é equivalente à abordagem 2 se o método foo () derereferenciar todos os argumentos de entrada em todos os caminhos de execução (o que provavelmente ocorre com frequência em métodos pequenos).
Resumindo
EDITAR
Obrigado a Stilgar para mais detalhes.
No seu caso, parece que você tem nulo como resultado de um método. Novamente, acho que você deve primeiro esclarecer (e corrigir) a semântica de seus métodos antes de tomar uma decisão.
Portanto, 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 ()? M2 () sempre deve retornar um resultado não nulo? Se for esse o caso, um resultado nulo é um erro interno (em m2 ()). Se você verificar o valor de retorno em m1 (), poderá ter um tratamento de erro mais robusto. Caso contrário, você 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 nulo, por que está retornando nulo? Talvez devesse lançar uma exceção? m2 () provavelmente é de 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 do método Javadoc. Nesse caso, não há problema em ter um valor nulo. O método m1 () deve verificar como qualquer outro valor: não há erro aqui, mas provavelmente verificar se o resultado é nulo é apenas parte da lógica do programa.
fonte
Minha abordagem geral é nunca testar uma condição de erro que você não sabe como lidar . Então a pergunta que precisa ser respondida em cada instância específica se torna: você pode fazer algo razoável na presença do
null
valor 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 de qualquer maneira. O exemplo de @ Shahbaz do World of Warcraft de substituir uma imagem por outra se enquadra nessa categoria; a imagem em si provavelmente é apenas decoração e não tem impacto funcional . (Em uma compilação de depuração, eu usaria uma cor gritante para chamar a atenção para o fato de que a substituição ocorreu, mas isso depende muito da natureza do aplicativo.) No entanto, registre detalhes sobre o erro, para que você saiba que ele está ocorrendo; caso contrário, você estará ocultando um erro que provavelmente deseja conhecer. Escondê-lo do usuário é uma coisa (novamente, supondo que o processamento possa continuar com segurança); escondê-lo do desenvolvedor é outra completamente diferente.
Se você não puder continuar com segurança na presença de um valor nulo, falhe com um erro sensível; em geral, prefiro falhar o mais rápido possível, quando é óbvio que a operação não pode ser bem-sucedida, o que implica verificar as pré-condições. Há momentos em que você não deseja falhar imediatamente, mas adia a falha pelo maior tempo possível; essa consideração surge, por exemplo, em aplicativos relacionados à criptografia, onde ataques de canal lateral podem divulgar informações que você não deseja que sejam conhecidas. No final, permita que o erro atinja algum manipulador de erro geral, que, por sua vez, registra detalhes relevantes e exibe uma boa (por mais agradável que seja) mensagem de erro para o usuário.
Também é importante notar que a resposta adequada pode muito bem diferir entre testes / controle de qualidade / compilações de depuração e produção / versão. Nas compilações de depuração, eu falharia cedo e muito 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 construções de produção, quando confrontadas com erros recuperáveis com segurança , provavelmente devem favorecer a recuperação.
fonte
Claro que
NULL
não é esperado , mas sempre há bugs!Acredito que você deve verificar
NULL
se não espera isso. Deixe-me dar exemplos (embora eles não estejam em Java).No World of Warcraft, devido a um bug, a imagem de um dos desenvolvedores apareceu em um cubo para muitos objetos. Imagine se o mecanismo gráfico não esperasse
NULL
indicadores, teria ocorrido um acidente, enquanto foi transformado em uma experiência não tão fatal (até engraçada) para o usuário. O mínimo é que você não teria perdido inesperadamente 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 bancário. 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 não receber
NULL
, um bug na interface pode causar um problema na transação, possivelmente perdendo algum dinheiro no meio (ou pior, duplicado).Por "um problema", quero dizer um travamento (como no travamento de travamento (digamos que o programa seja escrito em C ++), não uma exceção de ponteiro nulo). Nesse caso, a transação precisa reverter se NULL for encontrado (o que obviamente não seria possível se você não verificasse as variáveis em relação a NULL!)
Tenho certeza que você entendeu.
A verificação da validade dos argumentos para suas funções não atrapalha seu código, pois há algumas verificações na parte superior da sua função (não distribuídas no meio) e aumenta muito a robustez.
Se você tiver que escolher entre "o programa informa ao usuário que ele falhou e normalmente é encerrado ou volta a um estado consistente, possivelmente criando um log de erros" e "Violação de acesso", você não escolheria o primeiro? Isso significa que todas as funções devem estar preparadas para serem chamadas por um código de buggy, em vez de esperar que tudo esteja correto, simplesmente porque sempre existem bugs.
fonte
Como as outras respostas apontaram Se você não espera
NULL
, deixe claro jogandoArgumentNullException
. Na minha opinião, quando você está depurando o projeto, ele ajuda a descobrir as falhas na lógica do seu programa mais cedo.Então, você lançará o seu software; se você restaurar essas
NullRefrences
verificações, não perderá nada na lógica do seu software, apenas o dobro da certeza de que um bug grave não será exibido.Outro ponto é que, se a
NULL
verificação estiver nos parâmetros de uma função, você poderá decidir se a função é o local certo para fazê-lo 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, revise os cenários prováveis que podem levar a uma referência nula.fonte
Cada uma
null
de seu sistema é uma bomba-relógio esperando para ser descoberta. Verifiquenull
nos limites onde seu código interage com o código que você não controla e proíbanull
dentro do seu próprio código. Isso o livra do problema sem confiar ingenuamente no código estrangeiro. Use exceções ou algum tipo deMaybe
/Nothing
tipo.fonte
Embora uma exceção de ponteiro nulo seja lançada, muitas vezes ela será lançada longe do ponto em que você obteve o ponteiro nulo. Faça um favor a si mesmo e reduza o tempo que leva para corrigir o bug registrando pelo menos o fato de que você obtém um ponteiro nulo o mais rápido possível.
Mesmo se você não souber o que fazer agora, registrar o evento economizará tempo mais tarde. E talvez o cara que recebe o ingresso consiga usar o tempo economizado para descobrir a resposta adequada.
fonte
Como o
Fail early, fail fast
@DeadMG (@empi) não é pssível, pois o aplicativo Web deve funcionar bem com erros, você deve aplicar a regranenhuma exceção capturando sem registrar
para que você, como desenvolvedores, esteja ciente dos possíveis problemas, inspecionando os logs.
se você estiver usando uma estrutura de log como log4net ou log4j, poderá configurar uma saída de log especial adicional (também conhecida como appender)
que enviará erros e erros fatais. para que você fique informado.[atualizar]
se o usuário final da página não estiver ciente do erro, você poderá configurar um appender que envie por e-mail os erros e os erros fatais que permanecerão informados.
se estiver correto que o usuário final da página veja mensagens de erro enigmáticas, você pode configurar um aplicativo que coloca o log de erros para o processamento da página no final da página.
Não importa como você usa o log, se você engolir a exceção, o programa continua com dados que fazem sentido e a deglutição não é mais silenciosa.
fonte
Já existem boas respostas para essa pergunta, pode ser uma adição a elas: prefiro afirmações no meu código. Se o seu código puder funcionar apenas com objetos válidos, mas porca com nulo, você deve declarar o valor nulo.
As afirmações nesse tipo de situação permitiriam que qualquer teste de unidade e / ou integração falhasse com a mensagem exata, para que você possa resolver o problema rapidamente antes da produção. Se esse erro passar pelos testes e for para a produção (onde as asserções devem ser desativadas), você receberá NpEx e um bug grave, mas é melhor do que algum bug menor, que é muito mais confuso e aparece em algum outro lugar do código e seria mais caro de corrigir.
Além disso, se alguém precisar trabalhar com suas asserções de código, ele lhe informa sobre suas suposições feitas durante o design / gravação de seu código (neste caso: Ei, cara, esse objeto deve ser apresentado!), O que facilita a manutenção.
fonte
Normalmente, procuro nulos, mas "manipulo" nulos inesperados lançando uma exceção que fornece um pouco mais de detalhes sobre exatamente onde o nulo ocorreu.
Edit: Se você receber um nulo quando não espera que algo esteja errado - o programa deve morrer.
fonte
Depende da implementação do idioma das exceções. As exceções permitem que você execute alguma ação para evitar danos aos dados ou liberar recursos de forma limpa, caso o sistema entre em um estado ou condição imprevisível. Isso é diferente da manipulação de erros, condição que você pode antecipar. Minha filosofia é que você deve ter o mínimo de exceção possível. É barulho. Seu método pode fazer algo razoável ao lidar com a exceção? Ele pode se recuperar? Ele pode reparar ou evitar mais danos? Se você apenas capturar a exceção e retornar do método ou falhar de alguma outra maneira inofensiva, não havia realmente sentido em capturar a exceção. Você acrescentaria apenas ruído e complexidade ao seu método e talvez oculte a fonte de uma falha no seu design. Nesse caso, deixaria a falha borbulhar e seria pega por um loop externo. Se você pode fazer algo como reparar um objeto ou marcá-lo como corrompido ou liberar algum recurso crítico, como um arquivo de bloqueio ou fechar um soquete de maneira limpa, esse é um bom uso de exceções. Se você realmente espera que o NULL seja exibido com frequência como um caso de borda válido, você deve lidar com isso no fluxo lógico normal com instruções if-the-else ou switch-case ou qualquer outra coisa, 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 sequência vazia que representa um campo deixado em branco. Esta é uma área em que você deve usar o bom senso e o bom senso. Eu nunca ouvi falar de boas e sólidas regras práticas sobre como lidar com esse problema em todas as situações. Se você pode fazer algo como reparar um objeto ou marcá-lo como corrompido ou liberar algum recurso crítico, como um arquivo de bloqueio ou fechar um soquete de maneira limpa, esse é um bom uso de exceções. Se você realmente espera que o NULL seja exibido com frequência como um caso de borda válido, você deve lidar com isso no fluxo lógico normal com instruções if-the-else ou switch-case ou qualquer outra coisa, 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 sequência vazia que representa um campo deixado em branco. Esta é uma área em que você deve usar o bom senso e o bom senso. Eu nunca ouvi falar de boas e sólidas regras práticas sobre como lidar com esse problema em todas as situações. Se você pode fazer algo como reparar um objeto ou marcá-lo como corrompido ou liberar algum recurso crítico, como um arquivo de bloqueio ou fechar um soquete de maneira limpa, esse é um bom uso de exceções. Se você realmente espera que o NULL seja exibido com frequência como um caso de borda válido, você deve lidar com isso no fluxo lógico normal com instruções if-the-else ou switch-case ou qualquer outra coisa, 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 sequência vazia que representa um campo deixado em branco. Esta é uma área em que você deve usar o bom senso e o bom senso. Eu nunca ouvi falar de boas e sólidas regras práticas sobre como lidar com esse problema em todas as situações.
O Java falha na implementação de manipulação de exceções com "exceções verificadas", onde todas as exceções possíveis que possam ser geradas 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 lidar com exceções como achar melhor. Em Java, se você modificar o corpo de um método para incluir uma chamada que possa gerar uma exceção, você terá a opção de incluir manipulação explícita para uma exceção que talvez você não se importe em adicionar ruído e complexidade ao código, ou você deve adicione essa exceção à cláusula "throws" do método que você está modificando, que não apenas adiciona ruído e confusão, mas altera a assinatura do seu método. Você deve, então, ir um código de modificação onde quer que seu método seja usado para lidar com a nova exceção que seu método agora pode gerar ou adicionar a exceção à cláusula "throws" desses métodos, provocando um efeito dominó das alterações de código. O "Requisito de captura ou especificação" deve ser um dos aspectos mais irritantes do Java. A exceção de exceção para RuntimeExceptions e a racionalização oficial do Java para esse erro de design é coxo.
Esse último parágrafo teve pouco a ver com responder à sua pergunta. Eu odeio Java.
- Noah
fonte