Essa é uma das regras que são repetidas várias vezes e que me deixa perplexo.
Os nulos são maus e devem ser evitados sempre que possível .
Mas, mas - da minha ingenuidade, deixe-me gritar - às vezes um valor PODE estar significativamente ausente!
Por favor, deixe-me perguntar isso em um exemplo que vem desse código horrível e anti-padrão no qual estou trabalhando no momento . Este é, em essência, um jogo multiplayer baseado em turnos na web, onde os turnos de ambos os jogadores são executados simultaneamente (como em Pokemon e, ao contrário, no xadrez).
Após cada turno, o servidor transmite uma lista de atualizações para o código JS do lado do cliente. A classe Update:
public class GameUpdate
{
public Player player;
// other stuff
}
Essa classe é serializada para JSON e enviada para players conectados.
A maioria das atualizações naturalmente tem um jogador associado a elas - afinal, é necessário saber qual jogador fez qual jogada nesse turno. No entanto, algumas atualizações não podem ter um Player significativamente associado a elas. Exemplo: O jogo foi empatado com força porque o limite de turno sem ações foi excedido. Para essas atualizações, acho que é significativo ter o jogador anulado.
Claro, eu poderia "consertar" esse código utilizando herança:
public class GameUpdate
{
// stuff
}
public class GamePlayerUpdate : GameUpdate
{
public Player player;
// other stuff
}
No entanto, não vejo como isso melhora alguma coisa, por dois motivos:
- O código JS agora simplesmente receberá um objeto sem o Player como uma propriedade definida, que é a mesma que se fosse nula, pois os dois casos exigem verificação se o valor está presente ou ausente;
- Se outro campo anulável for adicionado à classe GameUpdate, eu precisaria usar herança múltipla para continuar com esse projeto - mas o MI é maligno por si só (de acordo com programadores mais experientes) e, mais importante, o C # não tê-lo, então eu não posso usá-lo.
Tenho a impressão de que este pedaço deste código é um dos muitos em que um bom programador experiente gritaria de horror. Ao mesmo tempo, não consigo ver como, neste lugar, esse nulo está prejudicando algo e o que deve ser feito.
Você poderia me explicar esse problema?
fonte
Respostas:
É melhor retornar muitas coisas do que nulo.
(que é o que fez você considerar nulo em primeiro lugar)
O truque é perceber quando fazer isso. O nulo é realmente bom em explodir na sua cara, mas apenas quando você o pontilha sem verificar. Não é bom explicar o porquê.
Quando você não precisar explodir e não desejar verificar se é nulo, use uma dessas alternativas. Pode parecer estranho retornar uma coleção se ela tiver apenas 0 ou 1 elementos, mas é realmente bom permitir que você lide silenciosamente com os valores ausentes.
Mônadas soam sofisticadas, mas aqui tudo o que estão fazendo é permitir que você torne óbvio que os tamanhos válidos para a coleção são 0 e 1. Se você os tiver, considere-os.
Qualquer um deles faz um trabalho melhor em tornar clara sua intenção do que nulo. Essa é a diferença mais importante.
Pode ajudar a entender por que você está retornando, em vez de simplesmente lançar uma exceção. Exceções são essencialmente uma maneira de rejeitar uma suposição (elas não são a única maneira). Quando você pergunta o ponto em que duas linhas se cruzam, você assume que elas se cruzam. Eles podem ser paralelos. Você deve lançar uma exceção "linhas paralelas"? Bem, você poderia, mas agora precisa lidar com essa exceção em outro lugar ou deixá-la interromper o sistema.
Se você preferir ficar onde está, pode transformar a rejeição dessa suposição em um tipo de valor que possa expressar resultados ou rejeições. Não é tão estranho assim. Fazemos isso em álgebra o tempo todo. O truque é tornar esse valor significativo para que possamos entender o que aconteceu.
Se o valor retornado puder expressar resultados e rejeições, ele precisará ser enviado para um código que possa lidar com ambos. O polimorfismo é realmente poderoso para usar aqui. Em vez de simplesmente trocar verificações nulas por
isPresent()
verificações, você pode escrever um código que se comporte bem em ambos os casos. Substituindo valores vazios por valores padrão ou silenciosamente sem fazer nada.O problema com null é que isso pode significar muitas coisas. Então isso acaba destruindo informações.
No meu experimento de pensamento nulo favorito, peço que você imagine uma máquina complexa Rube Goldbergiana que sinaliza mensagens codificadas usando luz colorida pegando lâmpadas coloridas de uma correia transportadora, enroscando-as em um soquete e ligando-as. O sinal é controlado pelas diferentes cores das lâmpadas colocadas na correia transportadora.
Você foi solicitado a tornar essa coisa horrivelmente cara compatível com a RFC3.14159, que afirma que deve haver um marcador entre as mensagens. O marcador não deve ter luz. Deve durar exatamente um pulso. A mesma quantidade de tempo que uma única luz colorida brilha normalmente.
Você não pode simplesmente deixar espaços entre as mensagens porque a engenhoca aciona alarmes e pára se não conseguir encontrar uma lâmpada para colocar no soquete.
Todo mundo familiarizado com esse projeto estremece ao tocar no mecanismo ou no código de controle. Não é fácil mudar. O que você faz? Bem, você pode mergulhar e começar a quebrar coisas. Talvez atualize esse design hediondo. Sim, você poderia torná-lo muito melhor. Ou você pode conversar com o zelador e começar a coletar lâmpadas queimadas.
Essa é a solução polimórfica. O sistema nem precisa saber que algo mudou.
É muito bom se você conseguir isso. Ao codificar uma rejeição significativa de uma suposição em um valor, você não precisa forçar a pergunta a mudar.
Quantas maçãs você me deve se eu lhe der uma maçã? -1. Porque te devia 2 maçãs de ontem. Aqui, a suposição da pergunta "você me deve" é rejeitada com um número negativo. Embora a pergunta esteja errada, informações significativas são codificadas nesta resposta. Ao rejeitá-lo de maneira significativa, a questão não é forçada a mudar.
No entanto, às vezes, mudar a pergunta é realmente o caminho a percorrer. Se a suposição puder ser mais confiável, ainda que útil, considere fazer isso.
Seu problema é que, normalmente, as atualizações vêm dos jogadores. Mas você descobriu a necessidade de uma atualização que não vem do jogador 1 ou do jogador 2. Você precisa de uma atualização que diga que o tempo expirou. Nenhum jogador está dizendo isso, então o que você deve fazer? Deixar o jogador nulo parece tão tentador. Mas isso destrói informações. Você está tentando codificar o conhecimento em um buraco negro.
O campo do jogador não diz quem acabou de se mudar. Você acha que sim, mas esse problema prova que é uma mentira. O campo player indica a origem da atualização. Sua atualização expirada está chegando no relógio do jogo. Minha recomendação é dar ao relógio o crédito que merece e alterar o nome do
player
campo para algo comosource
.Dessa forma, se o servidor estiver desligando e precisar suspender o jogo, ele poderá enviar uma atualização que relate o que está acontecendo e se liste como a fonte da atualização.
Isso significa que você nunca deve deixar algo de fora? Não. Mas você precisa considerar como nada pode ser assumido como realmente significando um único valor padrão bom e conhecido. Nada é realmente fácil de usar em excesso. Portanto, tenha cuidado ao usá-lo. Existe uma escola de pensamento que abraça a favor de nada . É chamado de convenção sobre configuração .
Eu gosto disso, mas apenas quando a convenção é claramente comunicada de alguma forma. Não deve ser uma maneira de embaçar os novatos. Mas mesmo se você estiver usando isso, null ainda não é a melhor maneira de fazer isso. Nulo é um nada perigoso . Nulo finge ser algo que você pode usar até usá-lo, então abre um buraco no universo (quebra seu modelo semântico). A menos que abrir um buraco no universo seja o comportamento que você precisa, por que está mexendo com isso? Use outra coisa.
fonte
null
não é realmente o problema aqui. O problema é como a maioria das linguagens de programação atuais lida com informações ausentes; eles não levam esse problema a sério (ou pelo menos não fornecem o apoio e a segurança que a maioria dos terráqueos precisa para evitar as armadilhas). Isso leva a todos os problemas que aprendemos a temer (Javas NullPointerException, Segfaults, ...).Vamos ver como lidar com esse problema de uma maneira melhor.
Outros sugeriram o Padrão de Objeto Nulo . Embora eu ache que é aplicável às vezes, há muitos cenários em que simplesmente não existe um valor padrão de tamanho único. Nesses casos, os consumidores das informações possivelmente ausentes devem poder decidir por si mesmos o que fazer se essas informações estiverem ausentes.
Existem linguagens de programação que fornecem segurança contra ponteiros nulos (chamada segurança de vácuo) em tempo de compilação. Para dar alguns exemplos modernos: Kotlin, Rust, Swift. Nesses idiomas, o problema da falta de informações foi levado a sério e o uso de null é totalmente indolor - o compilador impedirá que você desreferencie ponteiros nulos.
Em Java, foram feitos esforços para estabelecer as anotações @ Nullable / @ NotNull junto com os plugins do compilador + IDE para alcançar o mesmo efeito. Não foi realmente bem-sucedido, mas está fora do escopo desta resposta.
Um tipo explícito e genérico que denota possível ausência é outra maneira de lidar com isso. Por exemplo, em Java existe
java.util.Optional
. Pode funcionar:no nível do projeto ou da empresa, você pode estabelecer regras / diretrizes
java.util.Optional
vez denull
A comunidade / ecossistema de idiomas pode ter encontrado outras maneiras de resolver esse problema melhor do que o compilador / intérprete.
fonte
Optional.isPresent
isPresent()
não é o uso recomendado de OpcionalNão vejo mérito na afirmação de que nulo é ruim. Eu verifiquei as discussões sobre isso e elas só me deixam impaciente e irritada.
Nulo pode ser usado errado, como um "valor mágico", quando na verdade significa alguma coisa. Mas você teria que fazer uma programação bastante distorcida para chegar lá.
Nulo deve significar "não existe", que não foi criado (ainda) ou (já) foi ou não foi fornecido se for um argumento. Não é um estado, está faltando tudo. Em uma configuração OO em que as coisas são criadas e destruídas o tempo todo, é uma condição válida e significativa diferente de qualquer estado.
Não é inteiramente verdade, posso receber um aviso por não verificar nulo antes de usar a variável. E não é o mesmo. Talvez eu não queira poupar os recursos de um objeto que não tem propósito. E, mais importante, terei que verificar a alternativa da mesma forma para obter o comportamento desejado. Se eu esquecer disso, prefiro obter uma NullReferenceException em tempo de execução do que algum comportamento indefinido / não intencional.
Há um problema a ser observado. Em um contexto multithread, você teria que se proteger contra as condições de corrida atribuindo uma variável local primeiro antes de executar a verificação de nulo.
Não, não significa / não significa nada, então não há nada para destruir. O nome e o tipo da variável / argumento sempre dirão o que está faltando, isso é tudo o que há para saber.
Editar:
Estou na metade do vídeo candied_orange vinculado em uma de suas outras respostas sobre programação funcional e é muito interessante. Eu posso ver a utilidade disso em certos cenários. A abordagem funcional que eu vi até agora, com trechos de código voando, normalmente me faz estremecer quando usado em uma configuração OO. Mas acho que tem o seu lugar. Algum lugar. E acho que haverá idiomas que farão um bom trabalho ocultando o que eu considero uma bagunça confusa sempre que o encontrar em C #, idiomas que fornecem um modelo limpo e viável.
Se esse modelo funcional é a sua mentalidade / estrutura, não há realmente nenhuma alternativa para levar adiante contratempos como descrições de erros documentadas e, finalmente, deixar o chamador lidar com o lixo acumulado que foi arrastado ao longo do caminho de volta ao ponto de iniciação. Aparentemente, é assim que a programação funcional funciona. Chame isso de limitação, chame de novo paradigma. Você pode considerar que é um hack para os discípulos funcionais que lhes permite continuar um pouco mais no caminho profano; você pode se deliciar com essa nova maneira de programar que abre novas possibilidades interessantes. Seja como for, parece-me inútil entrar nesse mundo, voltar à maneira tradicional de OO de fazer as coisas (sim, podemos lançar exceções, desculpe), escolher algum conceito,
Qualquer conceito pode ser usado da maneira errada. De onde eu estou, as "soluções" apresentadas não são alternativas para um uso apropriado de null. Eles são conceitos de outro mundo.
fonte
None
Representa….Sua pergunta.
Totalmente a bordo com você lá. No entanto, existem algumas advertências em que entrarei em um segundo. Mas primeiro:
Eu acho que essa é uma afirmação bem-intencionada, mas generalizada.
Nulos não são maus. Eles não são um antipadrão. Eles não são algo que deve ser evitado a todo custo. No entanto, os nulos são propensos a erros (da falha em verificar o nulo).
Mas não entendo como a falta de verificação de nulo é de alguma forma a prova de que nulo é inerentemente errado de usar.
Se nulo for inválido, os índices de matriz e ponteiros C ++ também serão. Eles não são. Eles são fáceis de dar um tiro no pé, mas não devem ser evitados a todo custo.
Para o restante desta resposta, vou adaptar a intenção de "nulos são maus" para um mais nítido " nulos devem ser tratados com responsabilidade ".
Sua solução
Embora eu concorde com sua opinião sobre o uso de null como regra global; Não concordo com o uso de null neste caso específico.
Para resumir o feedback abaixo: você está entendendo mal o propósito da herança e do polimorfismo. Embora seu argumento para usar nulo tenha validade, sua "prova" adicional do motivo pelo qual a outra maneira é ruim se baseia no uso indevido de herança, tornando seus pontos um tanto irrelevantes para o problema nulo.
Se essas atualizações forem significativamente diferentes (quais são), você precisará de dois tipos de atualização separados.
Considere mensagens, por exemplo. Você tem mensagens de erro e mensagens de bate-papo de MI. Mas, embora possam conter dados muito semelhantes (uma mensagem de seqüência de caracteres e um remetente), eles não se comportam da mesma maneira. Eles devem ser usados separadamente, como classes diferentes.
O que você está fazendo agora é diferenciar efetivamente entre dois tipos de atualização com base na nulidade da propriedade Player. Isso equivale a decidir entre uma mensagem IM e uma mensagem de erro com base na existência de um código de erro. Funciona, mas não é a melhor abordagem.
Seu argumento se baseia em erros de polimorfismo. Se você possui um método que retorna um
GameUpdate
objeto, geralmente ele não deve se importar em diferenciar entreGameUpdate
,PlayerGameUpdate
ouNewlyDevelopedGameUpdate
.Uma classe base precisa ter um contrato de trabalho completo, ou seja, suas propriedades são definidas na classe base e não na classe derivada (isto é, o valor dessas propriedades base pode, obviamente, ser substituído nas classes derivadas).
Isso torna seu argumento discutível. Em boas práticas, você nunca deve se importar que seu
GameUpdate
objeto tenha ou não um jogador. Se você está tentando acessar aPlayer
propriedade de aGameUpdate
, está fazendo algo ilógico.Idiomas digitados como C # nem sequer permitem que você tente acessar a
Player
propriedade aGameUpdate
porqueGameUpdate
simplesmente não possui a propriedade. O Javascript é consideravelmente mais relaxado em sua abordagem (e não requer pré-compilação), portanto não se preocupa em corrigi-lo e, em vez disso, explode em tempo de execução.Você não deve criar classes com base na adição de propriedades anuláveis. Você deve criar classes com base em tipos de atualizações de jogos funcionalmente exclusivos .
Como evitar os problemas com null em geral?
Eu acho que é relevante elaborar como você pode expressar significativamente a ausência de um valor. Essa é uma coleção de soluções (ou abordagens ruins) em nenhuma ordem específica.
1. Use null de qualquer maneira.
Essa é a abordagem mais fácil. No entanto, você está se inscrevendo para uma tonelada de verificações nulas e (quando não o faz) solucionando problemas de exceções de referência nula.
2. Use um valor sem sentido não nulo
Um exemplo simples aqui é
indexOf()
:Em vez de
null
, você obtém-1
. Em um nível técnico, esse é um valor inteiro válido. No entanto, um valor negativo é uma resposta sem sentido, pois se espera que os índices sejam números inteiros positivos.Logicamente, isso só pode ser usado para casos em que o tipo de retorno permite mais valores possíveis do que podem ser retornados de forma significativa (indexOf = números inteiros positivos; int = números inteiros negativos e positivos)
Para tipos de referência, você ainda pode aplicar esse princípio. Por exemplo, se você tiver uma entidade com um ID inteiro, poderá retornar um objeto fictício com seu ID definido como -1, em vez de nulo.
Você simplesmente precisa definir um "estado simulado" pelo qual possa medir se um objeto é realmente utilizável ou não.
Observe que você não deve confiar em valores de sequência predeterminados se não controlar o valor da sequência. Você pode definir o nome de um usuário como "nulo" quando quiser retornar um objeto vazio, mas encontrará problemas quando Christopher Null se inscrever no seu serviço .
3. Lance uma exceção.
Não. Apenas não. Exceções não devem ser tratadas para fluxo lógico.
Dito isto, há alguns casos em que você não deseja ocultar a exceção (referência nula), mas mostra uma exceção apropriada. Por exemplo:
Isso pode gerar um
PersonNotFoundException
(ou similar) quando não houver uma pessoa com ID 123.De certa forma, obviamente, isso é aceitável apenas nos casos em que a não localização de um item impossibilita o processamento posterior. Se não for possível encontrar um item e o aplicativo continuar, você NUNCA deve lançar uma exceção . Não posso enfatizar isso o suficiente.
fonte
A razão pela qual os nulos são ruins não é que o valor ausente seja codificado como nulo, mas que qualquer valor de referência possa ser nulo. Se você tem C #, provavelmente está perdido de qualquer maneira. Não vejo sentido em usar algum opcional lá porque um tipo de referência já é opcional. Mas, para Java, existem anotações @Notnull, que você parece poder configurar no nível do pacote , e para valores que podem ser perdidos, você pode usar a anotação @Nullable.
fonte
Nulos não são maus, não existe realisticamente nenhuma maneira de implementar nenhuma das alternativas sem, em algum momento, lidar com algo que parece nulo.
Nulos são, no entanto, perigosos . Assim como qualquer outra construção de baixo nível, se você errar, não há nada abaixo para pegá-lo.
Eu acho que existem muitos argumentos válidos contra o null:
Que se você já estiver em um domínio de alto nível, existem padrões mais seguros do que usar o modelo de baixo nível.
A menos que você precise das vantagens de um sistema de baixo nível e esteja preparado para ser cauteloso, talvez você não deva usar uma linguagem de baixo nível.
etc ...
Tudo isso é válido e, além disso, não é necessário escrever esforços para getters e setters, etc., como um motivo comum para não seguir esse conselho. Não é realmente surpreendente que muitos programadores tenham chegado à conclusão de que os nulos são perigosos e preguiçosos (uma combinação ruim!), Mas, como muitos outros conselhos "universais", eles não são tão universais quanto são redigidos.
As pessoas boas que escreveram a língua pensaram que seriam úteis para alguém. Se você entender as objeções e ainda achar que elas são certas para você, ninguém mais saberá melhor.
fonte
Java tem um
Optional
classe com umifPresent
+ lambda explícito para acessar qualquer valor com segurança. Isso também pode ser feito em JS:Vejo esta pergunta StackOverflow .
A propósito: muitos puristas acharão esse uso duvidoso, mas acho que não. Recursos opcionais existem.
fonte
java.lang.Optional
sernull
também não pode?Optional.of(null)
lançaria uma exceção,Optional.ofNullable(null)
dariaOptional.empty()
- o valor não existe.Optional<Type> a = null
?Optional<Optional<Type>> a = Optional.empty();
;) - Em outras palavras, em JS (e outros idiomas), essas coisas não serão possíveis. Scala com valores padrão serviria. Java talvez com um enum. JS duvido:Optional<Type> a = Optional.empty();
.null
ou vazio, para dois mais alguns métodos extras no objeto interposto. Isso é uma conquista e tanto.A CAR Hoare considerou nulo o seu "erro de bilhão de dólares". No entanto, é importante observar que ele achou que a solução não era "sem nulos em lugar algum", mas "ponteiros não-nulos como padrão e nula somente onde são semanticamente necessários".
O problema com null nas linguagens que repetem seu erro é que você não pode dizer que uma função espera dados não anuláveis; é fundamentalmente inexprimível no idioma. Isso força as funções a incluir muitos clichês desnecessários de manipulação nula, e esquecer uma verificação nula é uma fonte comum de bugs.
Para driblar essa falta fundamental de expressividade, algumas comunidades (por exemplo, a comunidade Scala) não usam null. No Scala, se você deseja um valor anulável do tipo
T
, adota um argumento do tipoOption[T]
e, se deseja um valor não anulável, apenas adota aT
. Se alguém passar um nulo para o seu código, ele explodirá. No entanto, considera-se que o código de chamada é o código de buggy.Option
é uma boa alternativa para nulo, porque os padrões comuns de manipulação de nulos são extraídos para funções de biblioteca como.map(f)
ou.getOrElse(someDefault)
.fonte