Melhor explicação para idiomas sem null


225

De vez em quando, quando os programadores estão reclamando de erros / exceções nulos, alguém pergunta o que fazemos sem nulo.

Tenho uma idéia básica da frieza dos tipos de opção, mas não tenho o conhecimento ou a habilidade de idiomas para melhor expressar isso. Qual é uma ótima explicação do seguinte, escrita de maneira acessível ao programador médio para o qual podemos apontar essa pessoa?

  • A indesejabilidade de ter referências / ponteiros ser anulável por padrão
  • Como os tipos de opção funcionam, incluindo estratégias para facilitar a verificação de casos nulos, como
    • correspondência de padrões e
    • compreensões monádicas
  • Solução alternativa como mensagem comendo nada
  • (outros aspectos que eu perdi)

11
Se você adicionar tags a esta pergunta para programação funcional ou F #, poderá obter respostas fantásticas.
Stephen Swensen

Eu adicionei tag de programação funcional, já que o tipo de opção veio do mundo ml. Prefiro não marcar F # (muito específico). BTW alguém com poderes de taxonomia precisa adicionar tags do tipo talvez ou do tipo opção.
Roman A. Taycher

4
suspeito que haja pouca necessidade de tags específicas. As tags são principalmente para permitir que as pessoas encontrem perguntas relevantes (por exemplo, "perguntas sobre as quais eu conheço muito e poderemos responder", e "programação funcional" é muito útil lá. Mas algo como "nulo" ou " opção do tipo" são muito menos úteis Poucas pessoas são susceptíveis de monitorar um. 'tag-tipo de opção' à procura de perguntas que podem responder;).
jalf

Não devemos esquecer que uma das principais razões para nulo é que os computadores evoluíram fortemente vinculados à teoria dos conjuntos. Nulo é um dos conjuntos mais importantes de toda a teoria dos conjuntos. Sem ele, algoritmos inteiros iriam quebrar. Por exemplo, execute uma classificação de mesclagem. Isso envolve quebrar uma lista pela metade várias vezes. E se a lista tiver 7 itens? Primeiro você o divide em 4 e 3. Em seguida, 2, 2, 2 e 1. Então 1, 1, 1, 1, 1, 1, 1 e .... null! Nulo tem um objetivo, apenas um que você não vê praticamente. Existe mais para o campo teórico.
Stevendesu 29/10/10

6
@steven_desu - eu discordo. Nos idiomas 'anuláveis', você pode ter uma referência a uma lista vazia [] e também uma referência de lista nula. Esta questão está relacionada à confusão entre os dois.
stusmith

Respostas:


433

Penso que o resumo sucinto de por que nulo é indesejável é que estados sem sentido não devem ser representáveis .

Suponha que eu esteja modelando uma porta. Pode estar em um dos três estados: aberto, fechado, mas desbloqueado, e fechado e bloqueado. Agora eu poderia modelá-lo ao longo das linhas de

class Door
    private bool isShut
    private bool isLocked

e está claro como mapear meus três estados nessas duas variáveis ​​booleanas. Mas isso deixa uma quarta, estado indesejado disponíveis: isShut==false && isLocked==true. Como os tipos que selecionei como minha representação admitem esse estado, devo dedicar esforço mental para garantir que a classe nunca entre nesse estado (talvez codificando explicitamente um invariante). Por outro lado, se eu estivesse usando uma linguagem com tipos de dados algébricos ou enumerações verificadas que me permitissem definir

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

então eu poderia definir

class Door
    private DoorState state

e não há mais preocupações. O sistema de tipos garantirá a existência de apenas três estados possíveis para uma instância class Door. É nisso que os sistemas de tipos são bons - excluindo explicitamente toda uma classe de erros no tempo de compilação.

O problema nullé que todo tipo de referência obtém esse estado extra em seu espaço que normalmente é indesejável. Uma stringvariável pode ser qualquer sequência de caracteres ou esse nullvalor extra maluco que não é mapeado no domínio do meu problema. Um Triangleobjeto tem três Points, que possuem Xe Yvalores, mas infelizmente o Points ou o Trianglepróprio pode ser esse valor nulo louco que não faz sentido para o domínio gráfico em que estou trabalhando. Etc.

Quando você pretende modelar um valor possivelmente inexistente, opte explicitamente por ele. Se a maneira que pretendo modelar as pessoas é que todos Persontêm a FirstNamee a LastName, mas apenas algumas pessoas têm MiddleNames, então eu gostaria de dizer algo como

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

onde stringaqui é assumido como um tipo não anulável. Então não há invariantes complicados para estabelecer e nenhum NullReferenceExceptions inesperado ao tentar calcular o tamanho do nome de alguém. O sistema de tipos garante que qualquer código que lide com as MiddleNamecontas pela possibilidade de existir None, enquanto qualquer código que lide com o FirstNamepossa assumir com segurança que existe um valor lá.

Por exemplo, usando o tipo acima, poderíamos criar esta função boba:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

sem preocupações. Por outro lado, em um idioma com referências anuláveis ​​para tipos como string, assumindo

class Person
    private string FirstName
    private string MiddleName
    private string LastName

você acaba criando coisas como

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

que explode se o objeto Person recebido não tiver o invariante de tudo ser não nulo ou

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

ou talvez

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

assumindo que o pprimeiro / o último esteja lá, mas o meio pode ser nulo, ou talvez você faça verificações que geram tipos diferentes de exceções, ou quem sabe o que. Todas essas loucas opções de implementação e coisas para se pensar surgem porque existe esse valor representável estúpido que você não quer ou precisa.

Nulo normalmente adiciona complexidade desnecessária. A complexidade é inimiga de todos os softwares e você deve se esforçar para reduzir a complexidade sempre que possível.

(Observe bem que há mais complexidade até para esses exemplos simples. Mesmo que a FirstNamenão possa ser null, a stringpode representar ""(a sequência vazia), que provavelmente também não é um nome de pessoa que pretendemos modelar. Como tal, mesmo com não- seqüências anuláveis, ainda pode ser o caso de estarmos "representando valores sem sentido". Novamente, você pode optar por combater isso por meio de invariantes e código condicional em tempo de execução ou usando o sistema de tipos (por exemplo, para ter um NonEmptyStringtipo). este último talvez seja desaconselhável (tipos "bons" geralmente são "fechados" em um conjunto de operações comuns e, por exemplo, NonEmptyStringnão são fechados em.SubString(0,0)), mas demonstra mais pontos no espaço de design. No final do dia, em qualquer sistema de tipos, existe alguma complexidade da qual será muito bom se livrar, e outra complexidade que é intrinsecamente mais difícil de se livrar. A chave para este tópico é que, em quase todos os sistemas de tipos, a mudança de "referências anuláveis ​​por padrão" para "referências não anuláveis ​​por padrão" é quase sempre uma mudança simples que torna o sistema de tipos muito melhor na luta contra a complexidade e descartando certos tipos de erros e estados sem sentido. Portanto, é muito louco que tantos idiomas continuem repetindo esse erro repetidamente.)


31
Re: nomes - De fato. E talvez você se preocupe em modelar uma porta que está aberta, mas com a fechadura da fechadura saindo, impedindo que a porta se feche. Há muita complexidade no mundo. A chave é não adicionar mais complexidade ao implementar o mapeamento entre "estados mundiais" e "estados do programa" em seu software.
Brian

59
Você nunca trancou as portas abertas?
Joshua

58
Não entendo por que as pessoas se preocupam com a semântica de um domínio específico. Brian representou as falhas com nulo de maneira concisa e simples; sim, ele simplificou o domínio do problema em seu exemplo, dizendo que todos têm nome e sobrenome. A pergunta foi respondida a um 'T', Brian - se você estiver em Boston, devo-lhe uma cerveja por todas as publicações que fizer aqui!
akaphenom

67
@akaphenom: obrigado, mas observe que nem todas as pessoas bebem cerveja (eu não bebo). Mas compreendo que você esteja apenas usando um modelo simplificado do mundo para comunicar gratidão, por isso não discutirei mais sobre as suposições defeituosas do seu modelo-mundo. : P (Tanta complexidade no mundo real :)!)
Brian

4
Estranhamente, existem 3 portas estaduais neste mundo! Eles são usados ​​em alguns hotéis como portas de banheiro. Um botão de pressão atua como uma chave por dentro, que trava a porta por fora. Ele é desbloqueado automaticamente assim que o parafuso da trava se move.
comonad 24/10/10

65

O bom dos tipos de opções não é que eles são opcionais. É que todos os outros tipos não são .

Às vezes , precisamos ser capazes de representar um tipo de estado "nulo". Às vezes, precisamos representar uma opção "sem valor", bem como os outros valores possíveis que uma variável pode assumir. Portanto, uma linguagem que não permita isso será um pouco prejudicada.

Mas , muitas vezes , não precisamos disso, e permitir um estado "nulo" apenas leva a ambiguidade e confusão: toda vez que eu acesso uma variável de tipo de referência no .NET, devo considerar que ela pode ser nula .

Freqüentemente, nunca será realmente nulo, porque o programador estrutura o código para que isso nunca aconteça. Mas o compilador não pode verificar isso e, toda vez que o vê, você deve se perguntar "isso pode ser nulo? Preciso verificar aqui nulo?"

Idealmente, nos muitos casos em que nulo não faz sentido, não deve ser permitido .

Isso é difícil de obter no .NET, onde quase tudo pode ser nulo. Você precisa confiar no autor do código que está chamando para ser 100% disciplinado e consistente e documentar claramente o que pode e o que não pode ser nulo, ou você deve ser paranóico e verificar tudo .

No entanto, se os tipos não são anuláveis por padrão , você não precisa verificar se eles são nulos. Você sabe que eles nunca podem ser nulos, porque o compilador / verificador de tipos impõe isso a você.

E, em seguida, só precisamos de uma porta dos fundos para os casos raros em que fazer necessidade de lidar com um estado nulo. Em seguida, um tipo de "opção" pode ser usado. Em seguida, permitimos nulo nos casos em que tomamos uma decisão consciente de que precisamos ser capazes de representar o caso "sem valor" e, em todos os outros casos, sabemos que o valor nunca será nulo.

Como outros já mencionaram, em C # ou Java, por exemplo, null pode significar uma de duas coisas:

  1. a variável não foi inicializada. Idealmente, isso nunca deveria acontecer. Uma variável não deve existir, a menos que seja inicializada.
  2. a variável contém alguns dados "opcionais": precisa ser capaz de representar o caso em que não há dados . Isso às vezes é necessário. Talvez você esteja tentando encontrar um objeto em uma lista e não saiba com antecedência se ele está lá ou não. Então, precisamos ser capazes de representar que "nenhum objeto foi encontrado".

O segundo significado deve ser preservado, mas o primeiro deve ser totalmente eliminado. E mesmo o segundo significado não deve ser o padrão. É algo em que podemos optar por se e quando precisamos . Mas quando não precisamos que algo seja opcional, queremos que o verificador de tipos garanta que nunca será nulo.


E no segundo significado, queremos que o compilador nos avise (pare?) Se tentarmos acessar essas variáveis ​​sem verificar primeiro a nulidade. Aqui está um ótimo artigo sobre o próximo recurso C # nulo / não nulo (finalmente!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider

44

Até agora, todas as respostas se concentram no motivo de nullser uma coisa ruim e em como é útil se uma linguagem garantir que determinados valores nunca serão nulos.

Eles então sugerem que seria uma idéia bastante interessante se você aplicasse a nulidade para todos os valores, o que pode ser feito se você adicionar um conceito como Optionou Mayberepresentar tipos que nem sempre têm um valor definido. Essa é a abordagem adotada por Haskell.

É tudo de bom! Mas isso não impede o uso de tipos explicitamente anuláveis ​​/ não nulos para obter o mesmo efeito. Por que, então, a Option ainda é uma coisa boa? Afinal, Scala suporta valores anuláveis (é tem de, para que ele possa trabalhar com bibliotecas Java), mas suporta Optionsbem.

P. Então, quais são os benefícios além de poder remover nulos de um idioma completamente?

A. Composição

Se você fizer uma tradução ingênua do código com reconhecimento de nulo

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

para código com reconhecimento de opção

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

não há muita diferença! Mas também é uma maneira terrível de usar Opções ... Essa abordagem é muito mais limpa:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Ou até:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Quando você começa a lidar com a Lista de opções, fica ainda melhor. Imagine que a peopleprópria lista seja opcional:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Como é que isso funciona?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

O código correspondente com verificações nulas (ou mesmo elvis?: Operadores) seria dolorosamente longo. O verdadeiro truque aqui é a operação flatMap, que permite a compreensão aninhada de Opções e coleções de uma maneira que valores nulos nunca podem atingir.


8
+1, este é um bom ponto para enfatizar. Um adendo: em Haskell-land, flatMapseria chamado (>>=), isto é, o operador "bind" de mônadas. É isso mesmo, os haskellers gostam tanto de flatMappingar que colocamos no logotipo da nossa linguagem.
CA McCann

1
+1 Esperamos que uma expressão de Option<T>nunca, jamais seja nula. Infelizmente, Scala é uhh, ainda ligada ao Java :-) (Por outro lado, se Scala não jogar bonito com Java, que o usariam Oo?)

Fácil o suficiente: 'List (null) .headOption'. Observe que isso significa algo muito diferente do que o valor de retorno 'None'
Kevin Wright

4
Eu te dei uma recompensa, já que eu realmente gosto do que você disse sobre composição, que outras pessoas não pareciam mencionar.
Roman A. Taycher

Excelente resposta com ótimos exemplos!
thSoft 30/08/11

38

Desde que as pessoas parecem estar perdendo: nullé ambígua.

A data de nascimento de Alice é null. O que isso significa?

A data da morte de Bob é null. O que isso significa?

Uma interpretação "razoável" pode ser que a data de nascimento de Alice existe, mas é desconhecida, enquanto a data de morte de Bob não existe (Bob ainda está vivo). Mas por que chegamos a respostas diferentes?


Outro problema: nullé um caso extremo.

  • É null = null?
  • É nan = nan?
  • É inf = inf?
  • É +0 = -0?
  • É +0/0 = -0/0?

As respostas são geralmente "sim", "não", "sim", "sim", "não", "sim", respectivamente. Os "matemáticos" loucos chamam NaN de "nulidade" e dizem que se compara igual a si mesmo. O SQL trata nulos como não iguais a nada (portanto, eles se comportam como NaNs). Alguém se pergunta o que acontece quando você tenta armazenar ± ∞, ± 0 e NaNs na mesma coluna do banco de dados (existem 2 53 NaNs, metade dos quais é "negativo").

Para piorar a situação, os bancos de dados diferem na maneira como tratam NULL, e a maioria deles não é consistente (consulte Manipulação NULL no SQLite para obter uma visão geral). Isso é horrível.


E agora a história obrigatória:

Projetei recentemente uma tabela de banco de dados (sqlite3) com cinco colunas a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Como é um esquema genérico projetado para resolver um problema genérico para aplicativos bastante arbitrários, existem duas restrições de exclusividade:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aexiste apenas para compatibilidade com um design de aplicativo existente (em parte porque eu não encontrei uma solução melhor) e não é usado no novo aplicativo. Devido à forma como NULL trabalha em SQL, posso inserir (1, 2, NULL, 3, t)e (1, 2, NULL, 4, t)e não violar a primeira restrição de exclusividade (porque (1, 2, NULL) != (1, 2, NULL)).

Isso funciona especificamente por causa de como o NULL funciona com uma restrição de exclusividade na maioria dos bancos de dados (presumivelmente, é mais fácil modelar situações do "mundo real", por exemplo, duas pessoas não podem ter o mesmo número de seguridade social, mas nem todas as pessoas têm um).


FWIW, sem primeiro chamar um comportamento indefinido, as referências do C ++ não podem "apontar para" nulas e não é possível construir uma classe com variáveis-membro de referência não inicializadas (se uma exceção for lançada, a construção falhará).

Nota: Ocasionalmente, você pode querer ponteiros mutuamente exclusivos (ou seja, apenas um deles pode ser não NULL), por exemplo, em um iOS hipotético type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Em vez disso, sou forçado a fazer coisas assim assert((bool)actionSheet + (bool)alertView == 1).


Os matemáticos atuais não usam o conceito de "NaN", tenha certeza.
Noldorin

@Oldoldin: Eles fazem, mas eles usam o termo "forma indeterminada".
IJ Kennedy

@IJKennedy: Essa é uma faculdade diferente, que eu sei muito bem, obrigado. Alguns 'NaN's podem representar forma indeterminada, mas como o FPA não faz raciocínio simbólico, equipará-lo a forma indeterminada é bastante enganador!
Noldorin

O que há de errado assert(actionSheet ^ alertView)? Ou o seu idioma não pode XOR bools?
cat

16

A indesejabilidade de ter referências / ponteiros é anulável por padrão.

Eu não acho que esse seja o principal problema com nulos, o principal problema com nulos é que eles podem significar duas coisas:

  1. A referência / ponteiro não foi inicializada: o problema aqui é o mesmo que a mutabilidade em geral. Por um lado, torna mais difícil analisar seu código.
  2. A variável sendo nula realmente significa algo: este é o caso que os tipos de opção realmente formalizam.

Os idiomas que suportam os tipos de opção geralmente também proíbem ou desencorajam o uso de variáveis ​​não inicializadas.

Como os tipos de opção funcionam, incluindo estratégias para facilitar a verificação de casos nulos, como a correspondência de padrões.

Para serem eficazes, os tipos de opção precisam ser suportados diretamente no idioma. Caso contrário, é necessário muito código da placa da caldeira para simulá-los. A correspondência de padrões e a inferência de tipos são dois recursos principais do idioma, facilitando o trabalho dos tipos de opção. Por exemplo:

Em F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

No entanto, em uma linguagem como Java sem suporte direto para os tipos de opção, teríamos algo como:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Solução alternativa como mensagem comendo nada

A "mensagem que come nada" do Objective-C não é tanto uma solução, mas uma tentativa de aliviar a dor de cabeça da verificação nula. Basicamente, em vez de lançar uma exceção de tempo de execução ao tentar chamar um método em um objeto nulo, a expressão é avaliada como nula. Suspendendo a descrença, é como se cada método de instância começasse if (this == null) return null;. Mas há perda de informações: você não sabe se o método retornou nulo porque é um valor de retorno válido ou porque o objeto é realmente nulo. É como engolir exceção e não progride para resolver os problemas com nulo descrito anteriormente.


Esta é uma irritação, mas o c # dificilmente é uma linguagem do tipo c.
Roman A. Taycher

4
Eu estava indo para Java aqui, já que o C # provavelmente teria uma solução melhor ... mas eu aprecio sua irritação, o que as pessoas realmente querem dizer é "uma linguagem com sintaxe inspirada em c". Fui em frente e substitui a declaração "c-like".
Stephen Swensen

Com linq, certo. Eu estava pensando em c # e não percebi isso.
Roman A. Taycher

1
Sim, principalmente com sintaxe inspirada em c, mas acho que também ouvi falar de linguagens de programação imperativas como python / ruby ​​com muito pouco na maneira de sintaxe c como denominada c-like por programadores funcionais.
Roman A. Taycher

11

A Assembléia nos trouxe endereços também conhecidos como ponteiros não digitados. C os mapeou diretamente como ponteiros digitados, mas introduziu o null de Algol como um valor exclusivo de ponteiro, compatível com todos os ponteiros digitados. O grande problema com null em C é que, como todo ponteiro pode ser nulo, nunca se pode usar um ponteiro com segurança sem uma verificação manual.

Em idiomas de nível superior, ter nulo é estranho, pois realmente transmite duas noções distintas:

  • Dizendo que algo não está definido .
  • Dizer que algo é opcional .

Ter variáveis ​​indefinidas é praticamente inútil e gera comportamento indefinido sempre que elas ocorrem. Suponho que todos concordarão que ter coisas indefinidas deve ser evitado a todo custo.

O segundo caso é opcional e é melhor fornecido explicitamente, por exemplo, com um tipo de opção .


Digamos que estamos em uma empresa de transporte e precisamos criar um aplicativo para ajudar a criar uma programação para nossos motoristas. Para cada motorista, armazenamos algumas informações, tais como: as cartas de condução que eles possuem e o número de telefone para ligar em caso de emergência.

Em C, poderíamos ter:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Como você observa, em qualquer processamento da nossa lista de drivers, teremos que verificar se há indicadores nulos. O compilador não irá ajudá-lo, a segurança do programa depende de seus ombros.

No OCaml, o mesmo código ficaria assim:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Digamos agora que queremos imprimir os nomes de todos os motoristas, juntamente com seus números de licença de caminhão.

Em C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

No OCaml, isso seria:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Como você pode ver neste exemplo trivial, não há nada complicado na versão segura:

  • É terser.
  • Você obtém garantias muito melhores e nenhuma verificação nula é necessária.
  • O compilador garantiu que você lidasse corretamente com a opção

Considerando que em C, você poderia ter esquecido uma verificação nula e boom ...

Nota: esses exemplos de código não foram compilados, mas espero que você tenha entendido as idéias.


Eu nunca tentei, mas en.wikipedia.org/wiki/Cyclone_%28programming_language%29 afirma permitir que indicadores não nulos para c.
Roman A. Taycher

1
Não concordo com a sua afirmação de que ninguém está interessado no primeiro caso. Muitas pessoas, especialmente aquelas nas comunidades de idiomas funcionais, estão extremamente interessadas nisso e desencorajam ou proíbem completamente o uso de variáveis ​​não inicializadas.
Stephen Swensen

Acredito NULLque em "referência que pode não apontar para nada" foi inventada para alguma linguagem Algol (a Wikipedia concorda, consulte en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Mas é claro que é provável que os programadores de montagem inicializem seus ponteiros para um endereço inválido (leia-se: Nulo = 0).

1
@ Stephen: Nós provavelmente quisemos dizer a mesma coisa. Para mim, eles desencorajam ou proíbem o uso de coisas não inicializadas, precisamente porque não faz sentido discutir coisas indefinidas, pois não podemos fazer nada são ou úteis com elas. Não teria nenhum interesse.
bltxd

2
como @tc. diz que null não tem nada a ver com assembly. Na montagem, os tipos geralmente não são anuláveis. Um valor carregado em um registro de uso geral pode ser zero ou pode ser algum número inteiro diferente de zero. Mas nunca pode ser nulo. Mesmo se você carregar um endereço de memória em um registro, nas arquiteturas mais comuns, não há representação separada do "ponteiro nulo". Isso é um conceito introduzido em linguagens de alto nível, como C.
jalf

5

O Microsoft Research possui um projeto interessante chamado

Nº de especificação

É uma extensão C # com tipo não nulo e algum mecanismo para verificar se seus objetos não são nulos , embora, IMHO, a aplicação do princípio do design por contrato possa ser mais apropriada e mais útil para muitas situações problemáticas causadas por referências nulas.


4

Vindo do background do .NET, sempre achei que o null tinha um ponto, é útil. Até eu conhecer as estruturas e como era fácil trabalhar com elas, evitando muito código clichê. Tony Hoare falando na QCon London em 2009, pediu desculpas por inventar a referência nula . Para citá-lo:

Eu chamo de erro do meu bilhão de dólares. Foi a invenção da referência nula em 1965. Naquela época, eu estava projetando o primeiro sistema abrangente de tipos para referências em uma linguagem orientada a objetos (ALGOL W). Meu objetivo era garantir que todo o uso de referências fosse absolutamente seguro, com a verificação realizada automaticamente pelo compilador. Mas não pude resistir à tentação de colocar uma referência nula, simplesmente porque era muito fácil de implementar. Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos. Nos últimos anos, vários analisadores de programas como o PREfix e o PREfast na Microsoft foram usados ​​para verificar as referências e dar avisos se houver um risco de que eles não sejam nulos. Linguagens de programação mais recentes, como Spec #, introduziram declarações para referências não nulas. Esta é a solução, que rejeitei em 1965.

Veja esta pergunta também em programadores



1

Eu sempre olhei para Nulo (ou nulo) como sendo a ausência de um valor .

Às vezes você quer isso, às vezes não. Depende do domínio com o qual você está trabalhando. Se a ausência for significativa: sem nome do meio, seu aplicativo poderá agir de acordo. Por outro lado, se o valor nulo não estiver presente: o primeiro nome é nulo, o desenvolvedor recebe a proverbial ligação telefônica às duas da manhã.

Também vi código sobrecarregado e complicado demais com verificações de nulo. Para mim, isso significa uma das duas coisas:
a) um bug mais alto na árvore de aplicativos
b) design incorreto / incompleto

Do lado positivo - Nulo é provavelmente uma das noções mais úteis para verificar se algo está ausente, e linguagens sem o conceito de nulo acabarão complicando demais as coisas na hora de validar os dados. Nesse caso, se uma nova variável não for inicializada, os ditos idiomas geralmente definirão variáveis ​​para uma sequência vazia, 0 ou uma coleção vazia. No entanto, se uma sequência vazia ou 0 ou coleção vazia forem valores válidos para o seu aplicativo - você terá um problema.

Às vezes, isso é contornado, inventando valores especiais / estranhos para os campos para representar um estado não inicializado. Mas o que acontece quando o valor especial é inserido por um usuário bem-intencionado? E não vamos entrar na bagunça que isso fará das rotinas de validação de dados. Se a linguagem suportasse o conceito nulo, todas as preocupações desapareceriam.


Oi @ Jon, é um pouco difícil segui-lo aqui. Finalmente percebi que, por valores "especiais / estranhos", você provavelmente quer dizer algo como 'indefinido' do Javascript ou 'NaN' do IEEE. Além disso, você realmente não aborda nenhuma das perguntas feitas pelo OP. E a afirmação de que "Nulo é provavelmente a noção mais útil para verificar se algo está ausente" está quase certamente errada. Os tipos de opção são uma alternativa bem considerada e com segurança de tipo para null.
Stephen Swensen

@ Stephen - Na verdade, olhando para trás sobre a minha mensagem, acho que toda a segunda metade deve ser movida para uma pergunta ainda a ser feita. Mas ainda digo que nulo é muito útil para verificar se algo está ausente.
26610 Jon

0

Às vezes, os idiomas de vetor podem se safar por não ter um nulo.

O vetor vazio serve como um nulo digitado neste caso.


Acho que entendi do que você está falando, mas você poderia listar alguns exemplos? Especialmente de aplicar várias funções a um valor possivelmente nulo?
Roman A. Taycher

A aplicação de uma transformação de vetor a um vetor vazio resulta em outro vetor vazio. FYI, SQL é principalmente uma linguagem vetorial.
Josué

1
OK, é melhor eu esclarecer isso. SQL é uma linguagem vetorial para linhas e uma linguagem de valor para colunas.
Josué
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.