Enviando uma mensagem para nil em Objective-C

107

Como um desenvolvedor Java que está lendo a documentação Objective-C 2.0 da Apple: Eu me pergunto o que " enviar uma mensagem para o zero " significa - sem falar em como é realmente útil. Pegando um trecho da documentação:

Existem vários padrões no Cocoa que se aproveitam desse fato. O valor retornado de uma mensagem para nulo também pode ser válido:

  • Se o método retornar um objeto, qualquer tipo de ponteiro, qualquer escalar inteiro de tamanho menor ou igual a sizeof (void *), um flutuante, um duplo, um duplo longo ou um longo longo, então uma mensagem enviada para nil retornará 0 .
  • Se o método retornar uma estrutura, conforme definido pelo Guia de chamada de função ABI do Mac OS X para ser retornada em registradores, uma mensagem enviada para nil retornará 0,0 para cada campo na estrutura de dados. Outros tipos de dados de estrutura não serão preenchidos com zeros.
  • Se o método retornar qualquer coisa diferente dos tipos de valor mencionados, o valor de retorno de uma mensagem enviada para nil é indefinido.

Java tornou meu cérebro incapaz de grocar a explicação acima? Ou há algo que estou perdendo que tornaria isso tão claro quanto o vidro?

Eu tenho a idéia de mensagens / receptores em Objective-C, estou simplesmente confuso sobre um receptor que por acaso é nil.

Ryan Delucchi
fonte
2
Eu também tinha um background em Java e fiquei apavorado com esse recurso legal no começo, mas agora acho que é absolutamente ADORÁVEL !;
Valentin Radu
1
Obrigado, essa é uma ótima pergunta. Você já viu os benefícios disso? Parece-me algo do tipo "não é um bug, é um recurso". Continuo recebendo bugs em que o Java apenas me golpeava com uma exceção, então eu sabia onde estava o problema. Não estou feliz em trocar a exceção de ponteiro nulo para salvar uma ou duas linhas de código trivial aqui e ali.
Maciej Trybiło

Respostas:

92

Bem, acho que pode ser descrito usando um exemplo muito artificial. Digamos que você tenha um método em Java que imprime todos os elementos em um ArrayList:

void foo(ArrayList list)
{
    for(int i = 0; i < list.size(); ++i){
        System.out.println(list.get(i).toString());
    }
}

Agora, se você chamar esse método assim: someObject.foo (NULL); você provavelmente obterá um NullPointerException quando tentar acessar a lista, neste caso, na chamada para list.size (); Agora, você provavelmente nunca chamaria someObject.foo (NULL) com o valor NULL assim. No entanto, você pode ter obtido sua ArrayList de um método que retorna NULL se ocorrer algum erro ao gerar a ArrayList como someObject.foo (otherObject.getArrayList ());

Claro, você também terá problemas se fizer algo assim:

ArrayList list = NULL;
list.size();

Agora, em Objective-C, temos o método equivalente:

- (void)foo:(NSArray*)anArray
{
    int i;
    for(i = 0; i < [anArray count]; ++i){
        NSLog(@"%@", [[anArray objectAtIndex:i] stringValue];
    }
}

Agora, se tivermos o seguinte código:

[someObject foo:nil];

temos a mesma situação em que Java produzirá um NullPointerException. O objeto nil será acessado primeiro em [anArray count] No entanto, em vez de lançar um NullPointerException, Objective-C simplesmente retornará 0 de acordo com as regras acima, de modo que o loop não será executado. No entanto, se definirmos o loop para executar um determinado número de vezes, primeiro enviaremos uma mensagem para anArray em [anArray objectAtIndex: i]; Isso também retornará 0, mas como objectAtIndex: retorna um ponteiro, e um ponteiro para 0 é nulo / NULL, NSLog será passado nulo a cada vez que passar pelo loop. (Embora NSLog seja uma função e não um método, ele imprime (nulo) se for passado um NSString nulo.

Em alguns casos, é melhor ter um NullPointerException, pois você pode dizer imediatamente que algo está errado com o programa, mas a menos que você pegue a exceção, o programa irá travar. (Em C, tentar cancelar a referência de NULL dessa forma faz com que o programa trave.) Em Objective-C, isso apenas causa um comportamento possivelmente incorreto em tempo de execução. No entanto, se você tiver um método que não quebra se retornar 0 / nil / NULL / uma estrutura zerada, isso evita que você tenha que verificar se o objeto ou os parâmetros são nulos.

Michael Buckley
fonte
33
Provavelmente, vale a pena mencionar que esse comportamento tem sido objeto de muito debate na comunidade Objective-C nas últimas décadas. A compensação entre "segurança" e "conveniência" é avaliada de forma diferente por pessoas diferentes.
Mark Bessey,
3
Na prática, há muita simetria entre passar mensagens para nil e como Objective-C funciona, especialmente no novo recurso de ponteiros fracos no ARC. Ponteiros fracos são zerados automaticamente. Portanto, projete sua API para que ela possa responder a 0 / nil / NIL / NULL etc.
Cthutu
1
Eu acho que se você fizer myObject->iVarisso irá travar, independente se for C com ou sem objetos. (desculpe gravedig.)
11684
3
@ 11684 Correto, mas ->não é mais uma operação Objective-C, mas sim um C-ismo genérico.
bbum de
1
A recente exploração de root / API de backdoor oculta do OSX é acessível para todos os usuários (não apenas administradores) por causa das mensagens Nil do obj-c.
dcow de
51

À mensagem nilnão faz nada e retorna nil, Nil, NULL, 0, ou 0.0.

Peter Hosey
fonte
41

Todas as outras postagens estão corretas, mas talvez seja o conceito que é importante aqui.

Em chamadas de método Objective-C, qualquer referência de objeto que pode aceitar um seletor é um destino válido para esse seletor.

Isso economiza MUITO "o objeto de destino é do tipo X?" código - contanto que o objeto receptor implemente o seletor, não faz absolutamente nenhuma diferença de que classe ele é! nilé um NSObject que aceita qualquer seletor - ele simplesmente não faz nada. Isso elimina muito do código "verificar se há nulo, não envie a mensagem se for verdadeiro". (O conceito "se ele aceita, ele implementa" também é o que permite criar protocolos , que são quase como interfaces Java: uma declaração de que se uma classe implementa os métodos declarados, ela está em conformidade com o protocolo.)

A razão para isso é eliminar o código monkey que não faz nada, exceto manter o compilador feliz. Sim, você obtém a sobrecarga de mais uma chamada de método, mas economiza tempo do programador , que é um recurso muito mais caro do que o tempo da CPU. Além disso, você está eliminando mais código e mais complexidade condicional de seu aplicativo.

Esclarecendo para os downvoters: você pode pensar que este não é um bom caminho a percorrer, mas é como a linguagem é implementada e é o idioma de programação recomendado em Objective-C (consulte as aulas de programação do iPhone em Stanford).

Joe McMahon
fonte
17

O que significa é que o tempo de execução não produz um erro quando objc_msgSend é chamado no ponteiro nil; em vez disso, retorna algum valor (geralmente útil). As mensagens que podem ter um efeito colateral não fazem nada.

É útil porque a maioria dos valores padrão é mais apropriada do que um erro. Por exemplo:

[someNullNSArrayReference count] => 0

Ou seja, nil parece ser a matriz vazia. Ocultar uma referência NSView nula não faz nada. Handy, hein?

Rico
fonte
12

Na citação da documentação, existem dois conceitos separados - talvez seja melhor se a documentação deixar isso mais claro:

Existem vários padrões no Cocoa que aproveitam esse fato.

O valor retornado de uma mensagem para nulo também pode ser válido:

O primeiro é provavelmente mais relevante aqui: normalmente, ser capaz de enviar mensagens para niltorna o código mais direto - você não precisa verificar se há valores nulos em todos os lugares. O exemplo canônico é provavelmente o método acessador:

- (void)setValue:(MyClass *)newValue {
    if (value != newValue) { 
        [value release];
        value = [newValue retain];
    }
}

Se o envio de mensagens para nilnão fosse válido, esse método seria mais complexo - você teria que ter duas verificações adicionais para garantir valuee newValuenão nilantes de enviar mensagens.

O último ponto (os valores retornados de uma mensagem niltambém são normalmente válidos), entretanto, adiciona um efeito multiplicador ao primeiro. Por exemplo:

if ([myArray count] > 0) {
    // do something...
}

Este código novamente não requer uma verificação de nilvalores e flui naturalmente ...

Dito isso, a flexibilidade adicional de poder enviar mensagens niltem algum custo. Existe a possibilidade de, em algum estágio, você escrever um código que falhe de maneira peculiar porque você não levou em consideração a possibilidade de que um valor possa ser nil.

mmalc
fonte
12

De Greg Parker 's local :

Se estiver executando o LLVM Compiler 3.0 (Xcode 4.2) ou posterior

Mensagens para zero com tipo de retorno | Retorna
Inteiros até 64 bits | 0
Ponto flutuante até duplo longo | 0,0
Ponteiros | nada
Structs | {0}
Qualquer tipo _Complex | {0, 0}
Heath Borders
fonte
9

Isso significa muitas vezes não ter que verificar se há objetos nulos em todos os lugares para segurança - particularmente:

[someVariable release];

ou, como observado, vários métodos de contagem e comprimento retornam 0 quando você tem um valor nulo, portanto, não é necessário adicionar verificações extras para nil por completo:

if ( [myString length] > 0 )

ou isto:

return [myArray count]; // say for number of rows in a table
Kendall Helmstetter Gelner
fonte
Tenha em mente que o outro lado da moeda é o potencial para bugs como "if ([myString length] == 1)"
hatfinch
Como isso é um bug? [comprimento de myString] retorna zero (nulo) se myString for nulo ... uma coisa que eu acho que pode ser um problema é [quadro de myView] que eu acho que pode dar a você algo estranho se myView for nulo.
Kendall Helmstetter Gelner
Se você projetar suas classes e métodos em torno do conceito de que os valores padrão (0, nulo, NÃO) significam "não útil", esta é uma ferramenta poderosa. Nunca preciso verificar se há nada em minhas cordas antes de verificar o comprimento. Para mim, string vazia é tão inútil quanto uma string nula quando estou processando texto. Também sou um desenvolvedor Java e sei que os puristas do Java evitarão isso, mas economiza muito código.
Jason Fuerstenberg,
6

Não pense em "o receptor ser nulo"; Eu concordo, isso é muito estranho. Se você está enviando uma mensagem para nil, não há receptor. Você está apenas enviando uma mensagem para o nada.

Como lidar com isso é uma diferença filosófica entre Java e Objective-C: em Java, isso é um erro; em Objective-C, é um ambiente autônomo.

benzado
fonte
Há uma exceção a esse comportamento em java, se você chamar uma função estática em um nulo, é equivalente a chamar a função na classe de tempo de compilação da variável (não importa se é nula).
Roman A. Taycher,
6

Mensagens ObjC que são enviadas para nil e cujos valores de retorno têm tamanho maior que sizeof (void *) produzem valores indefinidos em processadores PowerPC. Além disso, essas mensagens fazem com que valores indefinidos sejam retornados em campos de estruturas cujo tamanho é maior que 8 bytes nos processadores Intel também. Vincent Gable descreveu isso muito bem em sua postagem do blog

Nikita Zhuk
fonte
6

Eu não acho que nenhuma das outras respostas mencionou isso claramente: se você está acostumado com Java, deve ter em mente que, embora Objective-C no Mac OS X tenha suporte para manipulação de exceção, é um recurso de linguagem opcional que pode ser ligado / desligado com um sinalizador do compilador. Meu palpite é que esse design de "enviar mensagens para nilé seguro" antecede a inclusão do suporte ao tratamento de exceções na linguagem e foi feito com um objetivo semelhante em mente: os métodos podem retornar nilpara indicar erros, e como o envio de uma mensagem para nilgeralmente retornanilpor sua vez, isso permite que a indicação de erro se propague em seu código, para que você não precise verificá-la em cada mensagem. Você só precisa verificar se há pontos importantes. Pessoalmente, acho que a propagação e o tratamento de exceções são a melhor maneira de abordar esse objetivo, mas nem todos concordam com isso. (Por outro lado, eu, por exemplo, não gosto da exigência de Java de você ter que declarar quais exceções um método pode lançar, o que muitas vezes força você a propagar sintaticamente declarações de exceção em todo o seu código; mas isso é outra discussão.)

Eu postei uma resposta semelhante, mas mais longa, para a pergunta relacionada "Afirmar que toda criação de objeto foi necessária com sucesso no Objetivo C?" se você quiser mais detalhes.

Rinzwind
fonte
Eu nunca pensei sobre isso dessa forma. Este parece ser um recurso muito conveniente.
mk12
2
Bom palpite, mas historicamente impreciso sobre o motivo da decisão. O tratamento de exceções existia na linguagem desde o início, embora os manipuladores de exceção originais fossem bastante primitivos em comparação com o idioma moderno. Nil-eats-message foi uma escolha de design consciente derivada do comportamento opcional do objeto Nil em Smalltalk. Quando as APIs NeXTSTEP originais foram projetadas, o encadeamento de métodos era bastante comum e um nilretorno frequentemente usado para curto-circuitar uma cadeia em um NO-op.
bbum de
2

C não representa nada como 0 para valores primitivos e NULL para ponteiros (que é equivalente a 0 em um contexto de ponteiro).

Objective-C se baseia na representação de C de nada, adicionando nil. nil é um ponteiro de objeto para nada. Embora semanticamente distintos de NULL, eles são tecnicamente equivalentes um ao outro.

NSObjects recém-alocados começam a vida com seu conteúdo definido como 0. Isso significa que todos os ponteiros que o objeto tem para outros objetos começam como nil, portanto, é desnecessário, por exemplo, definir self. (Associação) = nil nos métodos init.

O comportamento mais notável de nil, entretanto, é que ele pode receber mensagens.

Em outras linguagens, como C ++ (ou Java), isso travaria seu programa, mas em Objective-C, invocar um método em nil retorna um valor zero. Isso simplifica muito as expressões, pois elimina a necessidade de verificar se há nulo antes de fazer qualquer coisa:

// For example, this expression...
if (name != nil && [name isEqualToString:@"Steve"]) { ... }

// ...can be simplified to:
if ([name isEqualToString:@"Steve"]) { ... }

Estar ciente de como o nil funciona em Objective-C permite que essa conveniência seja um recurso, e não um bug oculto em seu aplicativo. Certifique-se de se proteger contra casos em que os valores nulos são indesejados, verificando e retornando antecipadamente para falhar silenciosamente ou adicionando um NSParameterAssert para lançar uma exceção.

Fonte: http://nshipster.com/nil/ https://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocObjectsClasses.html (Enviando mensagem para nil).

Zee
fonte