Referências nulas são realmente uma coisa ruim?


161

Ouvi dizer que a inclusão de referências nulas nas linguagens de programação é o "erro do bilhão de dólares". Mas por que? Claro, eles podem causar NullReferenceExceptions, mas e daí? Qualquer elemento do idioma pode ser uma fonte de erros se usado incorretamente.

E qual é a alternativa? Suponho que, em vez de dizer isso:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Você poderia dizer o seguinte:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Mas como isso é melhor? De qualquer forma, se você esquecer de verificar se o cliente existe, você recebe uma exceção.

Suponho que um CustomerNotFoundException seja um pouco mais fácil de depurar do que um NullReferenceException por ser mais descritivo. Isso é tudo o que existe?


54
Eu ficaria cauteloso com a abordagem "se o cliente existir / então consiga o cliente", assim que você escrever duas linhas de código como essa, estará abrindo a porta para condições de corrida. Obter e, em seguida, verificar se há nulas / capturas de exceções são mais seguras.
Carson63000

26
Se pudéssemos eliminar a fonte de todos os erros de tempo de execução, seria garantido que nosso código fosse perfeito! Se as referências NULL em C # quebrar o seu cérebro, tente inválido, mas não-NULL, ponteiros em C;)
LnxPrgr3

existem alguns operadores de coalescência nula e de navegação segura em alguns idiomas
jk.

35
null por si só não é ruim. Um sistema de tipos que não faz diferença entre o tipo T e o tipo T + nulo é inválido.
Ingo

27
Voltando à minha própria pergunta, alguns anos depois, agora sou totalmente convertido da abordagem Opção / Talvez.
Tim Goodman

Respostas:


91

nulo é mau

Há uma apresentação na InfoQ sobre este tópico: Referências nulas: o erro de bilhões de dólares de Tony Hoare

Tipo de opção

A alternativa da programação funcional é usar um tipo de opção , que pode conter SOME valueou NONE.

Um bom artigo O padrão "Option" que discute o tipo de opção e fornece uma implementação para Java.

Também encontrei um relatório de bug para Java sobre esse problema: Adicione tipos de opções agradáveis ​​ao Java para evitar NullPointerExceptions . O recurso solicitado foi introduzido no Java 8.


5
Anulável <x> em C # é um conceito semelhante, mas fica muito aquém de ser uma implementação do padrão de opção. O principal motivo é que no .NET System.Nullable está limitado a tipos de valor.
8898 MattHavey

27
+1 Para o tipo de opção. Depois de se familiarizar com Haskell de Talvez , nulos começar a olhar estranho ...
Andres F.

5
O desenvolvedor pode cometer um erro em qualquer lugar; portanto, em vez da exceção do ponteiro nulo, você receberá a exceção da opção vazia. Como o último é melhor que o primeiro?
greenoldman

7
@ greenreenman não existe "exceção de opção vazia". Tente inserir valores NULL nas colunas do banco de dados definidas como NOT NULL.
Jonas

36
@greenoldman O problema com os nulos é que qualquer coisa pode ser nula e, como desenvolvedor, você precisa ser extremamente cauteloso. Um Stringtipo não é realmente uma string. É um String or Nulltipo. Os idiomas que aderem totalmente à idéia dos tipos de opção não permitem valores nulos em seu sistema de tipos, garantindo que, se você definir uma assinatura de tipo que exija String(ou qualquer outra coisa), ela deverá ter um valor. O Optiontipo existe para fornecer uma representação no nível de tipo de coisas que podem ou não ter um valor, para que você saiba onde deve lidar explicitamente com essas situações.
KChaloux

127

O problema é que, em teoria, qualquer objeto pode ser nulo e lançar uma exceção quando você tenta usá-lo, seu código orientado a objetos é basicamente uma coleção de bombas não explodidas.

Você está certo de que a manipulação de erros graciosa pode ser funcionalmente idêntica às ifinstruções de verificação nula . Mas o que acontece quando algo que você se convenceu não poderia ser um nulo é, de fato, um nulo? Kerboom. Aconteça o que acontecer a seguir, estou disposto a apostar que 1) não será gracioso e 2) você não gostará.

E não descarte o valor de "fácil de depurar". O código de produção maduro é uma criatura louca e extensa; qualquer coisa que lhe dê mais informações sobre o que deu errado e onde você pode economizar horas de escavação.


27
+1 Normalmente, os erros no código de produção precisam ser identificados o mais rápido possível, para que possam ser corrigidos (geralmente, erro de dados). Quanto mais fácil for depurar, melhor.

4
Essa resposta é a mesma se aplicada a qualquer um dos exemplos do OP. Ou você testa condições de erro ou não, e você as manipula normalmente ou não. Em outras palavras, remover referências nulas do idioma não altera as classes de erros que você experimentará, mas apenas altera sutilmente a manifestação desses erros.
Dash-tom-bang

19
+1 para bombas não explodidas. Com referências nulas, dizer public Foo doStuff()não significa "fazer coisas e retornar o resultado Foo" significa "fazer coisas e retornar o resultado Foo, OU retornar nulo, mas você não tem idéia se isso vai acontecer ou não". O resultado é que você precisa anular a verificação EM TODA PARTE PARA ter certeza. É possível remover nulos e usar um tipo ou padrão especial (como um tipo de valor) para indicar um valor sem sentido.
Matt Olenik 19/10/10

12
@greenoldman, seu exemplo é duplamente eficaz para demonstrar nosso argumento de que o uso de tipos primitivos (nulos ou inteiros) como modelos semânticos é uma prática inadequada. Se você possui um tipo para o qual números negativos não são uma resposta válida, crie uma nova classe de tipos para implementar o modelo semântico com esse significado. Agora todo o código para manipular a modelagem desse tipo não negativo está localizado em sua classe, isolado do resto do sistema, e qualquer tentativa de criar um valor negativo para ele pode ser capturada imediatamente.
Huperniketes

9
@greenoldman, você continua a provar que os tipos primitivos são inadequados para a modelagem. Primeiro foi sobre números negativos. Agora acabou o operador de divisão quando zero é o divisor. Se você sabe que não pode dividir por zero, pode prever que o resultado não será bonito e é responsável por garantir o teste e fornecer o valor "válido" apropriado para esse caso. Os desenvolvedores de software são responsáveis ​​por conhecer os parâmetros em que seu código opera e codificar adequadamente. É o que distingue engenharia de mexer.
Huperniketes

50

Existem vários problemas com o uso de referências nulas no código.

Primeiro, geralmente é usado para indicar um estado especial . Em vez de definir uma nova classe ou constante para cada estado, como normalmente são feitas especializações, o uso de uma referência nula está usando um tipo / valor com perda generalizado em massa .

Segundo, o código de depuração se torna mais difícil quando uma referência nula aparece e você tenta determinar o que o gerou , qual estado está em vigor e sua causa, mesmo que você possa rastrear seu caminho de execução upstream.

Terceiro, referências nulas introduzem caminhos de código adicionais a serem testados .

Quarto, quando referências nulas são usadas como estados válidos para parâmetros e valores de retorno, a programação defensiva (para estados causados ​​pelo design) exige que mais verificações de referências nulas sejam feitas em vários locais ... apenas por precaução.

Em quinto lugar, o tempo de execução do idioma já está executando verificações de tipo quando realiza uma pesquisa de seletor na tabela de métodos de um objeto. Portanto, você está duplicando o esforço verificando se o tipo de objeto é válido / inválido e solicitando que o tempo de execução verifique o tipo de objeto válido para chamar seu método.

Por que não usar o padrão NullObject para tirar proveito da verificação do tempo de execução, para que ele invoque métodos NOP específicos para esse estado (conforme a interface do estado regular), além de eliminar toda a verificação extra de referências nulas em toda a sua base de código?

Isso envolve mais trabalho , criando uma classe NullObject para cada interface com a qual você deseja representar um estado especial. Mas pelo menos a especialização é isolada para cada estado especial, em vez do código no qual o estado pode estar presente. IOW, o número de testes é reduzido porque você tem menos caminhos de execução alternativos em seus métodos.


7
... porque descobrir quem gerou (ou melhor, não se deu ao trabalho de alterar ou inicializar) os dados de lixo que estão preenchendo seu objeto supostamente útil é muito mais fácil do que rastrear uma referência NULL? Eu não compro, apesar de estar preso em terrenos estaticamente tipificados.
dash-tom-bang

Dados de lixo são muito mais raros do que referências nulas. Particularmente em idiomas gerenciados.
Dean Harding

5
-1 para Objeto Nulo.
13138 DeadMG

4
Os pontos (4) e (5) são sólidos, mas o NullObject não é um remédio. Você cairá em erros ainda piores de lógica de armadilha (fluxo de trabalho). Quando você conhece EXATAMENTE a situação atual, certamente pode ajudar, mas usá-la às cegas (em vez de nula) custará mais.
greenoldman

1
@ gnasher729, embora o tempo de execução do Objective-C não gere uma exceção de ponteiro nulo como Java e outros sistemas, não melhora o uso de referências nulas pelas razões descritas na minha resposta. Por exemplo, se não houver um NavigationController no [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];tempo de execução, ele será tratado como uma sequência de não operações, a visualização não será removida da tela e você poderá ficar sentado na frente do Xcode coçando a cabeça por horas tentando descobrir o porquê.
Huperniketes

48

Nulos não são tão ruins, a menos que você não os esteja esperando. Você deve especificar explicitamente no código que espera nulo, que é um problema de design de linguagem. Considere isto:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Se você tentar chamar métodos em a Customer?, deverá receber um erro em tempo de compilação. O motivo pelo qual mais idiomas não fazem isso (IMO) é que eles não permitem que o tipo de variável seja alterado dependendo do escopo em que está. Se o idioma puder lidar com isso, o problema poderá ser resolvido inteiramente dentro do diretório sistema de tipos.

Há também a maneira funcional de lidar com esse problema, usando os tipos de opção e Maybe, mas não estou tão familiarizado com ele. Eu prefiro assim, porque o código correto teoricamente só precisa de um caractere adicionado para compilar corretamente.


11
Uau, isso é muito legal. Além de detectar muito mais erros no momento da compilação, isso me poupa de ter que verificar a documentação para descobrir se os GetByLastNameretornos nulos se não foram encontrados (em vez de gerar uma exceção) - só consigo ver se ele retorna Customer?ou Customer.
Tim Goodman

14
@JBRWilkinson: Este é apenas um exemplo elaborado para mostrar a ideia, não um exemplo de uma implementação real. Na verdade, acho que é uma ideia bem legal.
Dean Harding

1
Você está certo de que não é o padrão de objeto nulo.
Dean Harding

13
Parece com o que Haskell chama de Maybe- A Maybe Customeré Just c(onde cé a Customer) ou Nothing. Em outros idiomas, é chamado Option- veja algumas das outras respostas sobre esta questão.
precisa saber é o seguinte

2
@ SimonBarker: você pode ter certeza se Customer.FirstNameé do tipo String(ao contrário de String?). Esse é o benefício real - apenas variáveis ​​/ propriedades que possuem um valor significativo não devem ser declaradas como anuláveis.
Yogu

20

Há muitas respostas excelentes que cobrem os sintomas infelizes de null, então eu gostaria de apresentar um argumento alternativo: nulo é uma falha no sistema de tipos.

O objetivo de um sistema de tipos é garantir que os diferentes componentes de um programa "se encaixem" adequadamente; um programa bem digitado não pode "sair dos trilhos" para um comportamento indefinido.

Considere um dialeto hipotético de Java, ou qualquer que seja a sua linguagem de tipo estaticamente preferida, em que é possível atribuir a sequência "Hello, world!"a qualquer variável de qualquer tipo:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

E você pode verificar variáveis ​​assim:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

Não há nada impossível nisso - alguém poderia criar uma linguagem assim, se quisesse. O valor especial não precisa ser "Hello, world!"- poderia ter sido o número 42, a tupla (1, 4, 9)ou, digamos null,. Mas por que você faria isso? Uma variável do tipo Foodeve conter apenas Foos - esse é o objetivo do sistema de tipos! nullnão é Foomais do que "Hello, world!"é. Pior, nullnão é um valor de nenhum tipo e não há nada que você possa fazer com isso!

O programador nunca pode ter certeza de que uma variável realmente contém ae Foonem o programa; para evitar um comportamento indefinido, ele deve verificar as variáveis "Hello, world!"antes de usá-las como Foos. Observe que fazer a verificação de string no snippet anterior não propaga o fato de que foo1 é realmente um Foo- barprovavelmente terá sua própria verificação também, apenas por segurança.

Compare isso com o uso de um tipo Maybe/ Optioncom correspondência de padrões:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Dentro da Just foocláusula, você e o programa sabem com certeza que nossa Maybe Foovariável realmente contém um Foovalor - essas informações são propagadas na cadeia de chamadas e barnão precisam fazer nenhuma verificação. Como esse Maybe Fooé um tipo distinto Foo, você é forçado a lidar com a possibilidade de que ele possa conter Nothing, para nunca ser pego de surpresa por a NullPointerException. Você pode raciocinar sobre seu programa com muito mais facilidade e o compilador pode omitir verificações nulas, sabendo que todas as variáveis ​​do tipo Foorealmente contêm Foos. Todo mundo ganha.


E quanto a valores nulos no Perl 6? Eles ainda são nulos, exceto que sempre são digitados. Ainda é um problema que você pode escrever my Int $variable = Int, onde Intestá o valor nulo do tipo Int?
Konrad Borowski

@xfix Eu não estou familiarizado com Perl, mas contanto que você não tem uma maneira de impor this type only contains integersao contrário this type contains integers and this other value, você vai ter um problema de cada vez que unicamente quer inteiros.
Doval

1
+1 para correspondência de padrões. Com o número de respostas acima mencionando os tipos de opção, você pensaria que alguém teria mencionado o fato de que um recurso de linguagem simples, como os correspondentes de padrões, pode torná-los muito mais intuitivos de trabalhar do que os nulos.
Jules

1
Eu acho que a ligação monádica é ainda mais útil do que a correspondência de padrões (embora, obviamente, a ligação para Maybe seja implementada usando a correspondência de padrões). Eu acho que é meio divertido línguas vendo como C # colocando sintaxe especial no para muitas coisas ( ??, ?.e assim por diante) para coisas que linguagens como Haskell implementar funções como biblioteca. Eu acho que é muito mais arrumado. null/ Nothingpropagation não é "especial" de forma alguma, por que deveríamos ter que aprender mais sintaxe para tê-lo?
Sara

14

(Jogando meu chapéu no ringue para uma pergunta antiga;))

O problema específico com null é que ele quebra a digitação estática.

Se eu tiver um Thing t, o compilador pode garantir que posso ligar t.doSomething(). Bem, a menos que tseja nulo no tempo de execução. Agora todas as apostas estão encerradas. O compilador disse que estava tudo bem, mas descobri muito mais tarde que tNÃO de fato doSomething(). Portanto, em vez de poder confiar no compilador para capturar erros de tipo, tenho que esperar até o tempo de execução para capturá-los. Eu poderia muito bem usar Python!

Portanto, de certa forma, null introduz a digitação dinâmica em um sistema estaticamente digitado, com resultados esperados.

A diferença entre dividir por zero ou logaritmo negativo etc. é que quando i = 0, ainda é um int. O compilador ainda pode garantir seu tipo. O problema é que a lógica aplica mal esse valor de uma maneira que não é permitida pela lógica ... mas se a lógica faz isso, é praticamente a definição de um bug.

(O compilador pode capturar alguns desses problemas, BTW. Como sinalizar expressões como i = 1 / 0no momento da compilação. Mas você realmente não pode esperar que o compilador siga uma função e garanta que todos os parâmetros sejam consistentes com a lógica da função)

O problema prático é que você faz muito trabalho extra e adiciona verificações nulas para se proteger em tempo de execução, mas e se você esquecer uma? O compilador impede você de atribuir:

String s = novo Inteiro (1234);

Então, por que deveria permitir a atribuição a um valor (nulo) ao qual interromperá a des-referência s?

Ao misturar "sem valor" com referências "digitadas" no seu código, você está sobrecarregando os programadores. E quando as NullPointerExceptions acontecem, rastreá-las pode ser ainda mais demorado. Em vez de confiar na digitação estática para dizer "isso é uma referência a algo esperado", você está deixando o idioma dizer "isso pode muito bem ser uma referência a algo esperado".


3
Em C, uma variável do tipo *intidentifica um int, ou NULL. Da mesma forma em C ++, uma variável do tipo *Widgetidentifica a Widget, uma coisa cujo tipo deriva de Widget, ou NULL. O problema com Java e derivados como C # é que eles usam o local de armazenamento Widgetcomo significante *Widget. A solução não é fingir que um *Widgetnão deve ser capaz de aguentar NULL, mas sim ter um Widgettipo de local de armazenamento adequado .
supercat

14

Um nullponteiro é uma ferramenta

não é um inimigo. Você apenas deve usá-los da maneira certa.

Pense apenas em quanto tempo leva para encontrar e corrigir um erro típico de ponteiro inválido , em comparação com a localização e a correção de um erro nulo de ponteiro. É fácil verificar um ponteiro null. É uma PITA verificar se um ponteiro não nulo aponta ou não para dados válidos.

Se ainda não estiver convencido, adicione uma boa dose de multithreading ao seu cenário e pense novamente.

Moral da história: não jogue as crianças na água. E não culpe as ferramentas pelo acidente. O homem que inventou o número zero há muito tempo era bastante esperto, já que naquele dia você poderia nomear o "nada". O nullponteiro não está tão longe disso.


EDIT: Embora o NullObjectpadrão pareça ser uma solução melhor do que as referências que podem ser nulas, ele apresenta problemas por conta própria:

  • Uma referência que mantém um NullObjectdeve (de acordo com a teoria) não faz nada quando um método é chamado. Assim, erros sutis podem ser introduzidos devido a referências erroneamente não atribuídas, que agora são garantidas como não nulas (yehaa!), Mas executam uma ação indesejada: nada. Com um NPE, é quase óbvio onde está o problema. Com um NullObjectcomportamento que de alguma forma (mas errado), apresentamos o risco de que os erros sejam detectados tarde demais. Isso não é acidentalmente semelhante a um problema de ponteiro inválido e tem consequências semelhantes: A referência aponta para algo que parece dados válidos, mas não introduz erros de dados e pode ser difícil de localizar. Para ser sincero, nesses casos, eu preferiria sem piscar um NPE que falha imediatamente, agora,sobre um erro lógico / de dados que de repente me atinge mais tarde na estrada. Lembre-se do estudo da IBM sobre o custo dos erros em função de qual estágio eles são detectados?

  • A noção de não fazer nada quando um método é chamado a NullObjectnão é válida, quando um getter de propriedade ou uma função é chamada, ou quando o método retorna valores via out. Digamos que o valor de retorno é um inte decidimos que "não fazer nada" significa "retornar 0". Mas como podemos ter certeza de que 0 é o "nada" certo a ser (não) retornado? Afinal, ele NullObjectnão deve fazer nada, mas, quando solicitado por um valor, tem que reagir de alguma forma. Falhar não é uma opção: usamos o NullObjectpara impedir o NPE, e certamente não o trocaremos contra outra exceção (não implementada, dividida por zero, ...), não é? Então, como você não devolve nada corretamente quando precisa devolver alguma coisa?

Não posso ajudar, mas quando alguém tenta aplicar o NullObjectpadrão a todos os problemas, parece mais uma tentativa de reparar um erro cometendo outro. É sem dúvida uma solução boa e útil em alguns casos, mas certamente não é a bala mágica para todos os casos.


Você pode apresentar um argumento para o porquê de nullexistir? É possível fazer ondas de mão em praticamente qualquer falha de idioma com "não é um problema, apenas não se engane".
Doval

Para qual valor você definiria um ponteiro inválido?
21414 JensG

Bem, você não os define para nenhum valor, porque você não deve permitir. Em vez disso, você usa uma variável de um tipo diferente, talvez chamada, Maybe Tque pode conter um Nothingou um Tvalor. O principal aqui é que você é forçado a verificar se um Mayberealmente contém um Tantes de poder usá-lo e fornecer um curso de ação, caso contrário. E como você não permitiu ponteiros inválidos, agora você nunca precisa verificar nada que não seja um Maybe.
Doval

1
@JensG em seu exemplo mais recente, o compilador faz com que você faça a verificação nula antes de cada chamada t.Something()? Ou ele tem alguma maneira de saber que você já fez a verificação e de não fazê-lo novamente? E se você fez a verificação antes de passar tpara este método? Com o tipo Option, o compilador sabe se você já fez a verificação ou não, examinando o tipo de t. Se for uma opção, você não a verificou, enquanto se for o valor desembrulhado, você o terá.
Tim Goodman

1
O que seu objeto nulo retorna quando solicitado por um valor?
JensG

8

Referências nulas são um erro porque permitem código não sensorial:

foo = null
foo.bar()

Existem alternativas, se você alavancar o sistema de tipos:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

Geralmente, a idéia é colocar a variável em uma caixa, e a única coisa que você pode fazer é desempacotá-la, preferencialmente recrutando a ajuda do compilador como proposta para Eiffel.

Haskell tem isso do zero ( Maybe), em C ++ você pode aproveitar, boost::optional<T>mas ainda pode ter um comportamento indefinido ...


6
-1 para "alavancagem"!
Mark C

8
Mas certamente muitas construções de programação comuns permitem código sem sentido, não? A divisão por zero é (sem dúvida) absurda, mas ainda permitimos a divisão, e esperamos que o programador se lembre de verificar se o divisor é diferente de zero (ou manipula a exceção).
Tim Goodman

3
Eu não estou certo o que você quer dizer com "a partir do zero", mas Maybenão está embutido no núcleo da linguagem, a sua definição só acontece de ser no prelúdio padrão: data Maybe t = Nothing | Just t.
Fredoverflow 18/10/10

4
@FredOverflow: como o prelúdio é carregado por padrão, eu consideraria "do zero", que Haskell possui um sistema de tipos surpreendente o suficiente para permitir que essa (e outras) sutilezas sejam definidas com as regras gerais da linguagem, é apenas um bônus :)
Matthieu M.

2
@TimGoodman Embora a divisão por zero possa não ser o melhor exemplo para todos os tipos de valores numéricos (a IEEE 754 define um resultado para a divisão por zero), nos casos em que isso geraria uma exceção, em vez de ter valores do tipo Tdivididos por outros valores do tipo T, você limitaria a operação a valores do tipo Tdivididos por valores do tipo NonZero<T>.
22416 kbolino

8

E qual é a alternativa?

Tipos opcionais e correspondência de padrões. Como eu não conheço C #, aqui está um código em uma linguagem fictícia chamada Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}

6

O problema com os nulos é que as linguagens que lhes permitem forçar você a programar defensivamente contra ele. É preciso muito esforço (muito mais do que tentar usar blocos de defesa defensivos) para garantir que

  1. os objetos que você espera que eles não sejam nulos nunca são nulos, e
  2. que seus mecanismos defensivos lidem efetivamente com todos os potenciais EPIs.

Então, de fato, os nulos acabam sendo uma coisa cara de se ter.


1
Pode ser útil se você incluiu um exemplo em que é preciso muito mais esforço para lidar com nulos. No meu exemplo admitidamente simplificado, o esforço é praticamente o mesmo. Não estou discordando de você, apenas encontro exemplos de códigos específicos (pseudo) que mostram uma imagem mais clara do que as declarações gerais.
Tim Goodman

2
Ah, não me expliquei claramente. Não é que seja mais difícil lidar com nulos. É extremamente difícil (se não impossível) garantir que todo o acesso a referências potencialmente nulas já esteja protegido. Ou seja, em uma linguagem que permite referências nulas, é impossível garantir que um código de tamanho arbitrário esteja isento de exceções de ponteiro nulo, não sem grandes dificuldades e esforços. É isso que torna as referências nulas uma coisa cara. É extremamente caro garantir a ausência completa de exceções de ponteiro nulo.
Luis.espinal 19/10/10

4

O problema em que grau sua linguagem de programação tenta provar a correção do seu programa antes de executá-lo. Em um idioma estaticamente digitado, você prova que possui os tipos corretos. Ao passar para o padrão de referências não anuláveis ​​(com referências anuláveis ​​opcionais), você pode eliminar muitos dos casos em que o nulo é passado e não deveria ser. A questão é se o esforço extra no tratamento de referências não anuláveis ​​vale o benefício em termos de correção do programa.


4

O problema não é muito nulo, é que você não pode especificar um tipo de referência não nulo em muitas linguagens modernas.

Por exemplo, seu código pode parecer

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Quando as referências de classe são repassadas, a programação defensiva incentiva você a verificar todos os parâmetros para nulo novamente, mesmo que você tenha verificado nulos imediatamente, especialmente ao construir unidades reutilizáveis ​​em teste. A mesma referência pode ser facilmente verificada quanto a nulos 10 vezes na base de código!

Não seria melhor se você pudesse ter um tipo anulável normal quando receber a coisa, lidar com nulo de vez em quando e depois passá-lo como um tipo não anulável para todas as suas pequenas funções e classes auxiliares sem verificar nulo todas as Tempo?

Esse é o objetivo da solução Option - não permitir ativar estados de erro, mas permitir o design de funções que implicitamente não podem aceitar argumentos nulos. Se a implementação "padrão" dos tipos é ou não anulável ou não anulável é menos importante do que a flexibilidade de ter as duas ferramentas disponíveis.

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Também está listado como o recurso # 2 mais votado para C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c


Penso que outro ponto importante sobre a Opção <T> é que ela é uma mônada (e, portanto, também é um endofuncor), para que você possa compor cálculos complexos sobre fluxos de valores contendo tipos opcionais sem verificar explicitamente Nonecada etapa do caminho.
Sara

3

Nulo é mau. No entanto, a falta de um nulo pode ser um mal maior.

O problema é que, no mundo real, você costuma ter uma situação em que não possui dados. Seu exemplo da versão sem nulos ainda pode explodir - ou você cometeu um erro lógico e esqueceu de procurar Goodman ou talvez Goodman tenha se casado quando você verificou e quando a procurou. (Isso ajuda a avaliar a lógica se você descobrir que Moriarty está observando cada pedaço do seu código e tentando enganá-lo.)

O que faz a pesquisa de Goodman quando ela não está lá? Sem um nulo, você deve retornar algum tipo de cliente padrão - e agora você está vendendo coisas para esse padrão.

Fundamentalmente, tudo se resume a saber se é mais importante que o código funcione, não importa o que ou que funcione corretamente. Se um comportamento impróprio é preferível a nenhum comportamento, você não deseja nulos. (Para um exemplo de como essa pode ser a escolha certa, considere o primeiro lançamento do Ariane V. Um erro não detectado / 0 fez com que o foguete fizesse uma curva difícil e a autodestruição disparou quando se desfez devido a isso. estava tentando calcular que, na verdade, não servia mais a nenhum propósito no instante em que o booster estivesse aceso - teria feito órbita apesar da rotina de devolver o lixo.)

Pelo menos 99 vezes em 100, eu escolheria usar nulos. Eu pularia de alegria ao notar a versão do eu deles, no entanto.


1
Esta é uma boa resposta. A construção nula na programação não é o problema, são todas as instâncias de valores nulos. Se você criar um objeto padrão, eventualmente todos os seus valores nulos serão substituídos por esses padrões e sua interface do usuário, banco de dados e todos os outros itens serão preenchidos com esses, o que provavelmente não será mais útil. Mas você vai dormir à noite, porque pelo menos o seu programa não está travando, eu acho.
22415 Chris McCall

2
Você assume que essa nullé a única (ou mesmo preferível) maneira de modelar a ausência de um valor.
Sara

1

Otimize para o caso mais comum.

Ter que verificar se há nulo o tempo todo é tedioso - você deseja apenas se apossar do objeto Customer e trabalhar com ele.

No caso normal, isso deve funcionar bem - você procura, pega o objeto e o usa.

No caso excepcional , em que você (aleatoriamente) procura um cliente pelo nome, sem saber se esse registro / objeto existe, você precisa de alguma indicação de que isso falhou. Nessa situação, a resposta é lançar uma exceção RecordNotFound (ou deixar o provedor SQL abaixo fazer isso por você).

Se você estiver em uma situação em que não sabe se pode confiar nos dados que chegam (o parâmetro), talvez porque foram inseridos por um usuário, você também pode fornecer o 'TryGetCustomer (name, out customer)' padronizar. Referência cruzada com int.Parse e int.TryParse.


Eu diria que você nunca sabe se pode confiar nos dados que chegam, porque estão entrando em uma função e é do tipo anulável. O código pode ser refatorado para tornar a entrada totalmente diferente. Portanto, se houver um nulo possível, você sempre deve verificar. Então você acaba com um sistema em que todas as variáveis ​​são verificadas como nulas todas as vezes antes de serem acessadas. Os casos em que não está marcado tornam-se a porcentagem de falha do seu programa.
Chris McCall

0

Exceções não são avaliadas!

Eles estão lá por uma razão. Se o seu código estiver com problemas, existe um padrão genial, chamado exceção , e isso indica que algo está errado.

Ao evitar o uso de objetos nulos, você está ocultando parte dessas exceções. Não estou cravando no exemplo do OP, em que ele converte a exceção de ponteiro nulo em exceção bem digitada; isso pode realmente ser bom, pois aumenta a legibilidade. No entanto, quando você usa a solução do tipo Option , como @Jonas apontou, está ocultando exceções.

Do que no seu aplicativo de produção, em vez de ser lançada uma exceção ao clicar no botão para selecionar a opção vazia, nada acontece. Em vez de ponteiro nulo, a exceção seria lançada e provavelmente obteríamos um relatório dessa exceção (como em muitos aplicativos de produção, temos esse mecanismo) e do que poderíamos corrigi-lo.

Tornar seu código à prova de balas, evitando exceções, é uma péssima idéia, fazer você codificar à prova de balas, corrigindo exceções, e esse é o caminho que eu escolheria.


1
Agora sei um pouco mais sobre o tipo Option do que quando postei a pergunta há alguns anos, já que agora as uso regularmente no Scala. O ponto da Option é que atribui Nonea algo que não é uma Option a um erro em tempo de compilação e, da mesma forma, usar um Optioncomo se tivesse um valor sem primeiro verificar se é um erro em tempo de compilação. Erros de compilação certamente são mais agradáveis ​​do que as exceções em tempo de execução.
Tim Goodman

Por exemplo: você não pode dizer s: String = None, precisa dizer s: Option[String] = None. E se sé uma opção, você não pode dizer s.length, no entanto, pode verificar se ela possui um valor e extrair o valor ao mesmo tempo, com a correspondência de padrões:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman

Infelizmente no Scala você ainda pode dizer s: String = null, mas esse é o preço da interoperabilidade com o Java. Geralmente, proibimos esse tipo de coisa em nosso código Scala.
Tim Goodman

2
No entanto, concordo com o comentário geral de que exceções (usadas corretamente) não são uma coisa ruim, e "consertar" uma exceção engolindo-a geralmente é uma péssima idéia. Porém, com o tipo Option, o objetivo é transformar exceções em erros em tempo de compilação, o que é melhor.
Tim Goodman

@TimGoodman é apenas uma tática de scala ou pode ser usado em outras linguagens como Java e ActionScript 3? Também seria bom se você pudesse editar sua resposta com um exemplo completo.
Ilya Gazman

0

Nullssão problemáticos porque devem ser verificados explicitamente, mas o compilador não pode avisar que você se esqueceu de procurá-los. Somente análises estáticas demoradas podem lhe dizer isso. Felizmente, existem várias boas alternativas.

Tire a variável fora do escopo. Com muita frequência, nullé usado como marcador de posição quando um programador declara uma variável muito cedo ou a mantém por muito tempo. A melhor abordagem é simplesmente livrar-se da variável. Não o declare até que você tenha um valor válido para inserir nele. Essa não é uma restrição tão difícil quanto você imagina, e torna seu código muito mais limpo.

Use o padrão de objeto nulo. Às vezes, um valor ausente é um estado válido para o seu sistema. O padrão de objeto nulo é uma boa solução aqui. Evita a necessidade de verificações explícitas constantes, mas ainda permite representar um estado nulo. Não é "ocultar uma condição de erro", como algumas pessoas afirmam, porque nesse caso um estado nulo é um estado semântico válido. Dito isto, você não deve usar esse padrão quando um estado nulo não for um estado semântico válido. Você deve apenas tirar sua variável do escopo.

Use uma opção Talvez /. Primeiro, isso permite que o compilador avise que você precisa verificar se há um valor ausente, mas faz mais do que substituir um tipo de verificação explícita por outro. Usando Options, você pode encadear seu código e continuar como se seu valor existisse, não precisando realmente verificar até o último minuto absoluto. No Scala, seu código de exemplo seria semelhante a:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

Na segunda linha, onde estamos construindo a mensagem incrível, não fazemos uma verificação explícita se o cliente foi encontrado e a beleza é que não precisamos . Não é até a última linha, que pode ser muitas linhas mais tarde, ou mesmo em um módulo diferente, onde nós nos preocupar explicitamente com o que acontece se o Optioné None. Não apenas isso, se tivéssemos esquecido de fazê-lo, não teria o tipo verificado. Mesmo assim, é feito de uma maneira muito natural, sem uma ifdeclaração explícita .

Compare isso com a null, onde o tipo verifica muito bem, mas é necessário fazer uma verificação explícita em todas as etapas ou todo o aplicativo explodir em tempo de execução, se você tiver sorte durante um caso de uso que seus testes de unidade exercem. Simplesmente não vale a pena o aborrecimento.


0

O problema central do NULL é que ele torna o sistema não confiável. Em 1980, Tony Hoare, no jornal dedicado ao seu Prêmio Turing, escreveu:

E assim, o melhor dos meus conselhos para os criadores e designers da ADA foi ignorado. ... Não permita que esse idioma em seu estado atual seja usado em aplicações onde a confiabilidade é crítica, como centrais nucleares, mísseis de cruzeiro, sistemas de alerta precoce, sistemas de defesa antimísseis de mísseis. O próximo foguete a se desviar como resultado de um erro de linguagem de programação pode não ser um foguete espacial exploratório em uma viagem inofensiva a Vênus: pode ser uma ogiva nuclear explodindo em uma de nossas próprias cidades. Uma linguagem de programação não confiável que gera programas não confiáveis ​​constitui um risco muito maior para o meio ambiente e para a sociedade do que carros inseguros, pesticidas tóxicos ou acidentes em usinas nucleares. Seja vigilante para reduzir o risco, não para aumentá-lo.

A linguagem ADA mudou muito desde então, no entanto, esses problemas ainda existem em Java, C # e em muitas outras linguagens populares.

É dever do desenvolvedor criar contratos entre um cliente e um fornecedor. Por exemplo, em C #, como em Java, você pode usar Genericspara minimizar o impacto da Nullreferência, criando somente leitura NullableClass<T>(duas Opções):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

e depois use-o como

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

ou use duas opções de estilo com métodos de extensão C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

A diferença é significativa. Como cliente de um método, você sabe o que esperar. Uma equipe pode ter a regra:

Se uma classe não for do tipo NullableClass, sua instância não deve ser nula .

A equipe pode fortalecer essa idéia usando Design por contrato e verificação estática no momento da compilação, por exemplo, com pré-condição:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

ou para uma string

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Essa abordagem pode aumentar drasticamente a confiabilidade do aplicativo e a qualidade do software. Design by Contract é um caso da lógica Hoare , que foi preenchida por Bertrand Meyer em seu famoso livro de construção de software orientado a objetos e na linguagem Eiffel em 1988, mas não é usado de maneira inválida na criação de software moderna.


0

Não.

A falha de um idioma em contabilizar um valor especial como nullé o problema do idioma. Se você observar idiomas como Kotlin ou Swift , que forçam o escritor a lidar explicitamente com a possibilidade de um valor nulo a cada encontro, não há perigo.

Em idiomas como o em questão, o idioma permite nullser um valor de qualquer tipo -ish , mas não fornece construções para reconhecer isso posteriormente, o que leva a que as coisas sejam nullinesperadas (especialmente quando passadas para você de outro lugar) e assim você obtém falhas. Esse é o mal: deixar você usar nullsem fazer você lidar com isso.


-1

Basicamente, a idéia central é que os problemas de referência de ponteiro nulo sejam capturados no tempo de compilação, em vez do tempo de execução. Portanto, se você estiver escrevendo uma função que utiliza algum objeto e não deseja que ninguém a chame com referências nulas, o tipo system deve permitir que você especifique esse requisito para que o compilador possa usá-lo para garantir que a chamada para sua função nunca compile se o chamador não atender aos seus requisitos. Isso pode ser feito de várias maneiras, por exemplo, decorando seus tipos ou usando tipos de opção (também chamados tipos anuláveis ​​em alguns idiomas), mas obviamente isso é muito mais trabalhoso para os designers de compiladores.

Eu acho que é compreensível por que, nas décadas de 1960 e 70, o designer de compiladores não foi all-in para implementar essa idéia porque as máquinas eram limitadas em recursos, os compiladores precisavam ser relativamente simples e ninguém sabia realmente o quão ruim isso seria. anos abaixo da linha.


-1

Eu tenho uma objeção simples contra null:

Ele quebra completamente a semântica do seu código, introduzindo ambiguidade .

Muitas vezes, você espera que o resultado seja de um determinado tipo. Isso é semanticamente correto. Digamos, você pediu ao banco de dados um usuário com um certo valor id, espera que o resultado seja de um determinado tipo (= user). Mas, e se não houver usuário desse id? Uma (ruim) solução é: agregar nullvalor. Portanto, o resultado é ambíguo : ou é um userou é null. Mas nullnão é o tipo esperado. E é aí que o cheiro do código começa.

Ao evitar null, você torna seu código semanticamente claro.

Sempre há maneiras de contornar null: refatoração para coleções Null-objects, optionalsetc.


-2

Se for semanticamente possível que um local de armazenamento de referência ou tipo de ponteiro seja acessado antes que o código seja executado para calcular um valor para ele, não há uma escolha perfeita sobre o que deve acontecer. Ter esses locais como padrão para referências de ponteiro nulo que podem ser lidas e copiadas livremente, mas que apresentarão falhas se forem desferenciadas ou indexadas, é uma opção possível. Outras opções incluem:

  1. Os locais são padrão para um ponteiro nulo, mas não permitem que o código os veja (ou mesmo determine que eles são nulos) sem travar. Possivelmente, fornece um meio explícito de definir um ponteiro como nulo.
  2. Os locais são padronizados para um ponteiro nulo e travam se for feita uma tentativa de leitura de um, mas fornecem um meio sem travamento de testar se um ponteiro é nulo. Também forneça um meio explícito de definir um ponteiro como nulo.
  3. Os locais são padrão para um ponteiro para alguma instância padrão fornecida pelo compilador do tipo indicado.
  4. Os locais são padrão para um ponteiro para alguma instância padrão fornecida pelo programa em particular do tipo indicado.
  5. Faça com que qualquer acesso de ponteiro nulo (não apenas operações de cancelamento de referência) chame alguma rotina fornecida pelo programa para fornecer uma instância.
  6. Projete a semântica do idioma para que não exista coleções de locais de armazenamento, exceto um compilador inacessível temporário, até que as rotinas de inicialização sejam executadas em todos os membros (algo como um construtor de matriz precisaria ser fornecido com uma instância padrão, uma função que retornaria uma instância, ou possivelmente um par de funções - uma para retornar uma instância e uma que seria chamada em instâncias construídas anteriormente se ocorrer uma exceção durante a construção da matriz).

A última opção pode ter algum apelo, especialmente se um idioma incluir tipos anuláveis ​​e não anuláveis ​​(pode-se chamar os construtores de matriz especiais para qualquer tipo, mas somente é necessário chamá-los ao criar matrizes de tipos não anuláveis), mas provavelmente não seria viável na época em que ponteiros nulos foram inventados. Das outras opções, nenhuma parece mais atraente do que permitir que ponteiros nulos sejam copiados, mas não desreferenciados ou indexados. A abordagem 4 pode ser conveniente ter como opção e deve ser bastante barata de implementar, mas certamente não deve ser a única opção. Exigir que os ponteiros, por padrão, apontem para algum objeto válido em particular é muito pior do que ter ponteiros como padrão para um valor nulo que pode ser lido ou copiado, mas não desreferenciado ou indexado.


-2

Sim, NULL é um design terrível , no mundo orientado a objetos. Em poucas palavras, o uso NULL leva a:

  • tratamento ad-hoc de erros (em vez de exceções)
  • semântica ambígua
  • lento em vez de rápido falhar
  • pensamento de computador vs. pensamento de objeto
  • objetos mutáveis ​​e incompletos

Verifique esta publicação no blog para obter uma explicação detalhada: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.