Um novo campo booleano é melhor que uma referência nula quando um valor pode estar significativamente ausente?

39

Por exemplo, suponha que eu tenho uma classe Member, que possui lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

cujo lastChangePasswordTime pode estar ausente de forma significativa, porque alguns membros nunca podem alterar suas senhas.

Mas, de acordo com Se nulos são maus, o que deve ser usado quando um valor pode estar significativamente ausente? e https://softwareengineering.stackexchange.com/a/12836/248528 , não devo usar null para representar um valor significativamente ausente. Então, eu tento adicionar um sinalizador booleano:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Mas acho que é bastante obsoleto porque:

  1. Quando isPasswordChanged é false, lastChangePasswordTime deve ser nulo e a verificação de lastChangePasswordTime == null é quase idêntica à verificação de isPasswordChanged é false, portanto, prefiro verificar lastChangePasswordTime == null diretamente

  2. Ao alterar a lógica aqui, posso esquecer de atualizar os dois campos.

Nota: quando um usuário altera as senhas, registro o horário da seguinte forma:

this.lastChangePasswordTime=Date.now();

O campo booleano adicional é melhor do que uma referência nula aqui?

ocomfd
fonte
51
Essa pergunta é bastante específica da linguagem, porque o que constitui uma boa solução depende em grande parte do que sua linguagem de programação oferece. Por exemplo, em C ++ 17 ou Scala, você usaria std::optionalou Option. Em outros idiomas, você pode ter que criar um mecanismo apropriado, você mesmo pode recorrer a nullalgo semelhante, porque é mais idiomático.
Christian Hackl
16
Existe um motivo para você não querer que o lastChangePasswordTime seja definido como o horário de criação da senha (afinal, a criação é um mod)?
Kristian H
@ChristianHackl hmm, concordo que existem diferentes soluções "perfeitas", mas não vejo nenhuma linguagem (principal), embora o uso de um booleano separado seja uma idéia melhor em geral do que fazer verificações nulas / nulas. Não tenho certeza absoluta sobre C / C ++, pois não estou ativo há um bom tempo.
Frank Hopkins
@FrankHopkins: Um exemplo seria a linguagem em que variáveis ​​podem ser deixadas não inicializadas, por exemplo, C ou C ++. lastChangePasswordTimepode ser um ponteiro não inicializado lá, e compará-lo a qualquer coisa seria um comportamento indefinido. Não é uma razão realmente convincente para não inicializar o ponteiro para NULL/ em nullptrvez disso, especialmente não no C ++ moderno (onde você não usaria um ponteiro), mas quem sabe? Outro exemplo seria linguagens sem ponteiros, ou talvez com um mau suporte para ponteiros. (FORTRAN 77 vem à mente ...)
Christian Hackl
Eu usaria um enum com 3 casos para isso :)
J. Doe

Respostas:

72

Não vejo por que, se você tem um valor significativamente ausente, nullnão deve ser usado se você for deliberado e cuidadoso.

Se seu objetivo é cercar o valor anulável para impedir que ele seja referenciado acidentalmente, sugiro criar o isPasswordChangedvalor como uma função ou propriedade que retorna o resultado de uma verificação nula, por exemplo:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

Na minha opinião, fazendo assim:

  • Oferece melhor legibilidade ao código do que uma verificação nula, o que pode perder o contexto.
  • Remove a necessidade de você realmente se preocupar em manter o isPasswordChangedvalor mencionado.

A maneira como você persiste os dados (presumivelmente em um banco de dados) seria responsável por garantir que os nulos sejam preservados.

TZHX
fonte
29
+1 para a abertura. O uso arbitrário de nulo geralmente é reprovado apenas nos casos em que nulo é um resultado inesperado, ou seja, onde não faria sentido (para um consumidor). Se nulo for significativo, não será um resultado inesperado.
Flater
28
Eu colocaria um pouco mais forte, pois nunca há duas variáveis ​​que mantêm o mesmo estado. Isso irá falhar. Ocultar um valor nulo, assumindo que o valor nulo faça sentido dentro da instância, de fora é uma coisa muito boa. Nesse caso, você poderá decidir posteriormente que 01-01-2009 indica que a senha nunca foi alterada. Então a lógica do resto do programa não precisa se importar.
Bent
3
Você pode esquecer de ligar, isPasswordChangedassim como você pode verificar se é nulo. Não vejo nada ganho aqui.
Gherman
9
@Gherman Se o consumidor não entender o conceito de que nulo é significativo ou precisar verificar o valor presente, ele estará perdido de qualquer maneira, mas se o método for usado, é claro para todos que leem o código por que há é uma verificação e existe uma chance razoável de que o valor seja nulo / não presente. Caso contrário, não está claro se foi apenas um desenvolvedor que adicionou uma verificação nula apenas "porque por que não" ou se faz parte do conceito. Claro, pode-se descobrir, mas de uma maneira que você tem as informações diretamente da outra, é necessário analisar a implementação.
Frank Hopkins
2
@FrankHopkins: Bom argumento, mas se a simultaneidade for usada, mesmo a inspeção de uma única variável pode exigir bloqueio.
Christian Hackl
63

nullsnão são maus. Usá-los sem pensar é. Este é um caso em que nullé exatamente a resposta correta - não há data.

Observe que sua solução cria mais problemas. Qual é o significado da data que está sendo definida como algo, mas o isPasswordChangedé falso? Você acabou de criar um caso de informações conflitantes que precisa capturar e tratar especialmente, enquanto um nullvalor tem um significado claramente definido e inequívoco e não pode estar em conflito com outras informações.

Portanto, não, sua solução não é melhor. Permitir um nullvalor é a abordagem correta aqui. As pessoas que afirmam que nullsempre são más, não importa o contexto, não entendem o porquê null.

Tom
fonte
17
Historicamente, nullexiste porque Tony Hoare cometeu o erro de um bilhão de dólares em 1965. As pessoas que afirmam nullser "más" estão simplificando demais o tópico, é claro, mas é bom que as linguagens modernas ou os estilos de programação estejam se afastando null, substituindo com tipos de opções dedicados.
Christian Hackl
9
@ChristianHackl: apenas por interesse histórico - o Die Hoare já disse qual seria a melhor alternativa prática (sem mudar para um paradigma completamente novo)?
AnoE 26/02
5
@AnoE: pergunta interessante. Não sei o que Hoare tinha a dizer sobre isso, mas a programação sem nulos costuma ser uma realidade hoje em dia em linguagens de programação modernas e bem projetadas.
Christian Hackl
10
@Tom wat? Em linguagens como C # e Java, o DateTimecampo é um ponteiro. Todo o conceito de nullsó faz sentido para ponteiros!
Todd Sewell
16
@ Tom: ponteiros nulos são perigosos apenas em linguagens e implementações que permitem indexar e desreferenciar cegamente, com qualquer hilaridade que possa resultar. Em uma linguagem que intercepta tentativas de indexar ou desreferenciar um ponteiro nulo, muitas vezes será menos perigoso que um ponteiro para um objeto fictício. Existem muitas situações em que ter um programa encerrado de forma anormal pode ser preferível a produzir números sem sentido e, em sistemas que os prendem, ponteiros nulos são a receita certa para isso.
supercat 26/02
29

Dependendo da sua linguagem de programação, pode haver boas alternativas, como um optionaltipo de dados. Em C ++ (17), isso seria std :: opcional . Pode estar ausente ou ter um valor arbitrário do tipo de dados subjacente.

dasmy
fonte
8
Também existe Optionalno java; o significado de um nulo Optionaldepende de você ...
Matthieu M.
1
Vim aqui para se conectar com o opcional também. Eu codificaria isso como um tipo em um idioma que os contenha.
Zipp 26/02
2
@FrankHopkins É uma pena que nada em Java impeça você de escrever Optional<Date> lastPasswordChangeTime = null;, mas eu não desistiria apenas por causa disso. Em vez disso, eu instilaria uma regra de equipe muito firme, de que nenhum valor opcional jamais pode ser atribuído null. Opcional, você compra muitos recursos interessantes para desistir facilmente. Você pode facilmente atribuir valores padrão ( orElseou com uma avaliação lenta :) orElseGet, lançar erros ( orElseThrow), transformar valores de forma nula e segura ( map, flatmap) etc.
Alexander - Reinstate Monica
5
A verificação @FrankHopkins isPresentquase sempre é um cheiro de código, na minha experiência, e um sinal bastante confiável de que os desenvolvedores que estão usando esses opcionais estão "perdendo o objetivo". De fato, basicamente não oferece nenhum benefício ref == null, mas também não é algo que você deva usar com frequência. Mesmo que a equipe seja instável, uma ferramenta de análise estática pode estabelecer a lei, como um linter ou FindBugs , que pode capturar a maioria dos casos.
Alexander - Restabelecer Monica
5
@FrankHopkins: Pode ser que a comunidade Java precise apenas de uma pequena mudança de paradigma, o que ainda pode levar muitos anos. Se você olhar para o Scala, por exemplo, ele suporta nullporque a linguagem é 100% compatível com Java, mas ninguém a usa e Option[T]está em todo o lado, com excelente suporte à programação funcional como map, flatMapcorrespondência de padrões e assim por diante. muito, muito além das == nullverificações. Você está certo que, em teoria, um tipo de opção parece pouco mais do que um ponteiro glorificado, mas a realidade prova que esse argumento está errado.
Christian Hackl
11

Corretamente usando null

Existem diferentes maneiras de usar null. A maneira mais comum e semanticamente correta é usá-lo quando você pode ou não ter um único valor. Nesse caso, um valor é igual nullou é significativo como um registro do banco de dados ou algo assim.

Nessas situações, você geralmente o usa dessa maneira (em pseudo-código):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Problema

E tem um problema muito grande. O problema é que, quando você invoca, doSomethingUsefulo valor pode não ter sido verificado null! Caso contrário, o programa provavelmente falhará. E o usuário pode nem ver boas mensagens de erro, ficando com algo como "erro horrível: valor desejado, mas foi nulo!" (após a atualização: embora possa haver ainda menos erros informativos como Segmentation fault. Core dumped., ou pior ainda, nenhum erro e manipulação incorreta em null em alguns casos)

Esquecer de escrever verificações nulle lidar com nullsituações é um bug extremamente comum . É por isso que Tony Hoare, que inventou, nulldisse em uma conferência de software chamada QCon London em 2009 que ele cometeu o erro de um bilhão de dólares em 1965: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Erro-Tony-Hoare

Evitando o problema

Algumas tecnologias e linguagens tornam nullimpossível verificar o esquecimento de maneiras diferentes, reduzindo a quantidade de bugs.

Por exemplo, Haskell tem a Maybemônada em vez de nulos. Suponha que esse DatabaseRecordseja um tipo definido pelo usuário. Em Haskell, um valor do tipo Maybe DatabaseRecordpode ser igual Just <somevalue>ou igual a Nothing. Você pode usá-lo de diferentes maneiras, mas não importa como o use, não poderá aplicar alguma operação Nothingsem conhecê-lo.

Por exemplo, esta função chamada zeroAsDefaultretorna xpara Just xe 0para Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl diz que C ++ 17 e Scala têm seus próprios caminhos. Portanto, você pode tentar descobrir se o seu idioma tem algo parecido e usá-lo.

Os nulos ainda são amplamente utilizados

Se você não tem nada melhor, o uso nullé bom. Apenas continue atento. Declarações de tipo em funções o ajudarão de alguma maneira.

Além disso, isso pode parecer pouco progressivo, mas você deve verificar se seus colegas querem usar nullou algo mais. Eles podem ser conservadores e podem não querer usar novas estruturas de dados por alguns motivos. Por exemplo, suporte a versões mais antigas de um idioma. Tais coisas devem ser declaradas nos padrões de codificação do projeto e discutidas adequadamente com a equipe.

Na sua proposta

Você sugere o uso de um campo booleano separado. Mas você precisa verificá-lo de qualquer maneira e ainda pode esquecer de verificá-lo. Portanto, não há nada ganho aqui. Se você pode esquecer outra coisa, como atualizar os dois valores a cada vez, é ainda pior. Se o problema de esquecer de verificar nullnão for resolvido, não faz sentido. Evitar nullé difícil e você não deve fazê-lo de uma maneira que a torne pior.

Como não usar null

Finalmente, existem maneiras comuns de usar nullincorretamente. Uma dessas maneiras é usá-lo no lugar de estruturas de dados vazias, como matrizes e seqüências de caracteres. Uma matriz vazia é uma matriz adequada como qualquer outra! É quase sempre importante e útil para estruturas de dados, que podem se ajustar a vários valores, para poder ficar vazio, ou seja, tem 0 comprimento.

Do ponto de vista da álgebra, uma string vazia para strings é semelhante a 0 para números, ou seja, identidade:

a+0=a
concat(str, '')=str

A string vazia permite que as strings em geral se tornem um monóide: https://en.wikipedia.org/wiki/Monoid Se você não conseguir, não é tão importante para você.

Agora vamos ver por que é importante programar com este exemplo:

for (element in array) {
  doSomething(element);
}

Se passarmos uma matriz vazia aqui, o código funcionará bem. Isso não fará nada. No entanto, se passarmos nullaqui, provavelmente teremos um erro com um erro do tipo "não é possível fazer um loop nulo, desculpe". Poderíamos envolvê-lo, ifmas isso é menos limpo e, novamente, você pode esquecer de verificá-lo

Como lidar com null

O que doSomethingAboutIt()deveria estar fazendo e, principalmente, se deveria lançar uma exceção é outra questão complicada. Em resumo, depende se nullfoi um valor de entrada aceitável para uma determinada tarefa e o que é esperado em resposta. Exceções são para eventos que não eram esperados. Não irei mais além nesse tópico. Essa resposta já é longa.

Gherman
fonte
5
Na verdade, horrible error: wanted value but got null!é muito melhor do que o mais típico Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight True, mas eu preferiria algo que se enquadre nos termos do usuário Error: the product you want to buy is out of stock.
Gherman 26/02
Claro - eu estava apenas observando que poderia ser (e geralmente é) ainda pior . Eu concordo completamente com o que seria melhor! E sua resposta é basicamente o que eu teria dito a esta pergunta: +1.
Toby Speight
2
@ Alemanha: Passar nullonde nullnão é permitido é um erro de programação, ou seja, um bug no código. Um produto que está fora de estoque faz parte da lógica comercial comum e não um bug. Você não pode e não deve tentar traduzir a detecção de um bug em uma mensagem normal na interface do usuário. Falha imediata e não execução de mais código é o curso de ação preferido, especialmente se for uma aplicação importante (por exemplo, uma que execute transações financeiras reais).
Christian Hackl
1
@EricDuminil Queremos remover (ou reduzir) a possibilidade de cometer um erro, não se remover nullcompletamente, mesmo do fundo.
Gherman
4

Além de todas as respostas muito boas fornecidas anteriormente, eu acrescentaria que toda vez que você for tentado a deixar um campo nulo, pense com cuidado se for um tipo de lista. Um tipo anulável é equivalente a uma lista de 0 ou 1 elementos, e geralmente isso pode ser generalizado para uma lista de N elementos. Especificamente, neste caso, você pode considerar lastChangePasswordTimeuma lista de passwordChangeTimes.

Joel
fonte
Esse é um ponto excelente. O mesmo acontece com os tipos de opção; uma opção pode ser vista como caso especial de uma sequência.
Christian Hackl
2

Pergunte a si mesmo: qual comportamento requer o campo lastChangePasswordTime?

Se você precisar desse campo para um método IsPasswordExpired () para determinar se um Membro deve ser solicitado a alterar sua senha de vez em quando, eu definiria o campo para o momento em que o Membro foi criado inicialmente. A implementação IsPasswordExpired () é a mesma para membros novos e existentes.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Se você tiver um requisito separado de que os membros recém-criados tenham que atualizar suas senhas, eu adicionaria um campo booleano separado chamado passwordShouldBeChanged e o definiria como verdadeiro na criação. Eu mudaria a funcionalidade do método IsPasswordExpired () para incluir uma verificação para esse campo (e renomearia o método para ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Faça suas intenções explícitas no código.

Rik D
fonte
2

Primeiro, nulo ser mau é dogma e, como de costume, funciona melhor como orientação e não como teste de aprovação / não aprovação.

Em segundo lugar, você pode redefinir sua situação de uma maneira que faça sentido que o valor nunca possa ser nulo. InititialPasswordChanged é um booleano definido inicialmente como false; PasswordSetTime é a data e hora em que a senha atual foi definida.

Observe que, embora isso tenha um pequeno custo, agora você pode calcular SEMPRE quanto tempo se passou desde a última vez que uma senha foi definida.

jmoreno
fonte
1

Ambos são 'seguros / sãos / corretos' se o chamador verificar antes do uso. A questão é o que acontece se o chamador não verificar. Qual é o melhor, algum sabor de erro nulo ou usando um valor inválido?

Não existe uma única resposta correta. Depende do que você está preocupado.

Se as falhas forem realmente ruins, mas a resposta não for crítica ou tiver um valor padrão aceito, talvez seja melhor usar um booleano como sinalizador. Se usar a resposta errada é um problema pior do que travar, usar o null é melhor.

Para a maioria dos casos "típicos", falhas rápidas e forçar os chamadores a verificar são a maneira mais rápida de codificar corretamente, e, portanto, acho que nulo deve ser a opção padrão. Eu não colocaria muita fé nos evangelistas "X é a raiz de todo mal"; eles normalmente não anteciparam todos os casos de uso.

ANone
fonte