Invio di un messaggio a zero in Objective-C


107

Come sviluppatore Java che sta leggendo la documentazione Objective-C 2.0 di Apple: mi chiedo cosa significhi " inviare un messaggio a zero ", figuriamoci come sia effettivamente utile. Prendendo un estratto dalla documentazione:

Ci sono diversi modelli in Cocoa che sfruttano questo fatto. Il valore restituito da un messaggio a nil può anche essere valido:

  • Se il metodo restituisce un oggetto, qualsiasi tipo di puntatore, qualsiasi intero scalare di dimensione inferiore o uguale a sizeof (void *), un float, un double, un long double o un long long, un messaggio inviato a nil restituisce 0 .
  • Se il metodo restituisce una struttura, come definita dalla Guida alla chiamata alla funzione ABI di Mac OS X, da restituire nei registri, un messaggio inviato a nil restituisce 0,0 per ogni campo nella struttura dati. Altri tipi di dati struct non verranno riempiti con zeri.
  • Se il metodo restituisce qualcosa di diverso dai suddetti tipi di valore, il valore di ritorno di un messaggio inviato a nil non è definito.

Java ha reso il mio cervello incapace di elaborare la spiegazione di cui sopra? O c'è qualcosa che mi manca che lo renderebbe chiaro come il vetro?

Ho l'idea di messaggi / ricevitori in Objective-C, sono semplicemente confuso su un ricevitore che sembra essere nil.


2
Avevo anche un background Java e all'inizio ero terrorizzato da questa simpatica funzionalità, ma ora la trovo assolutamente BELLA !;
Valentin Radu

1
Grazie, è un'ottima domanda. Hai visto i vantaggi di questo? Mi colpisce come una cosa "non un bug, una caratteristica". Continuo a ricevere bug in cui Java mi schiaffeggiava con un'eccezione, quindi sapevo dove fosse il problema. Non sono felice di scambiare l'eccezione del puntatore nullo per salvare una o due righe di codice banale qua e là.
Maciej Trybiło

Risposte:


92

Bene, penso che possa essere descritto usando un esempio molto artificioso. Supponiamo che tu abbia un metodo in Java che stampa tutti gli elementi in un ArrayList:

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

Ora, se chiami quel metodo in questo modo: someObject.foo (NULL); probabilmente otterrai una NullPointerException quando tenta di accedere a list, in questo caso nella chiamata a list.size (); Ora, probabilmente non chiameresti mai someObject.foo (NULL) con il valore NULL come quello. Tuttavia, potresti aver ottenuto il tuo ArrayList da un metodo che restituisce NULL se si verifica un errore nella generazione di ArrayList come someObject.foo (otherObject.getArrayList ());

Ovviamente, avrai problemi anche se fai qualcosa del genere:

ArrayList list = NULL;
list.size();

Ora, in Objective-C, abbiamo il metodo equivalente:

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

Ora, se abbiamo il seguente codice:

[someObject foo:nil];

abbiamo la stessa situazione in cui Java produrrà una NullPointerException. All'oggetto nil si accederà per primo a [anArray count] Tuttavia, invece di lanciare una NullPointerException, Objective-C restituirà semplicemente 0 in conformità con le regole precedenti, quindi il ciclo non verrà eseguito. Tuttavia, se impostiamo il ciclo in modo che venga eseguito un determinato numero di volte, per prima cosa inviamo un messaggio a anArray in [anArray objectAtIndex: i]; Anche questo restituirà 0, ma poiché objectAtIndex: restituisce un puntatore e un puntatore a 0 è nil / NULL, NSLog verrà passato a zero ogni volta nel ciclo. (Sebbene NSLog sia una funzione e non un metodo, stampa (null) se viene passato un NSString nullo.

In alcuni casi è più bello avere una NullPointerException, poiché puoi dire subito che qualcosa non va nel programma, ma a meno che non cogli l'eccezione, il programma andrà in crash. (In C, il tentativo di dereferenziare NULL in questo modo provoca il crash del programma.) In Objective-C, invece, provoca solo un comportamento di runtime forse errato. Tuttavia, se hai un metodo che non si interrompe se restituisce 0 / nil / NULL / una struttura azzerata, questo ti evita di dover controllare per assicurarti che l'oggetto o i parametri siano nulli.


33
Probabilmente vale la pena ricordare che questo comportamento è stato oggetto di molti dibattiti nella comunità di Objective-C negli ultimi due decenni. Il compromesso tra "sicurezza" e "convenienza" viene valutato in modo diverso da persone diverse.
Mark Bessey

3
In pratica c'è molta simmetria tra il passaggio di messaggi a zero e il funzionamento di Objective-C, specialmente nella nuova funzionalità dei puntatori deboli in ARC. I puntatori deboli vengono azzerati automaticamente. Quindi progetta la tua API in modo che possa rispondere a 0 / nil / NIL / NULL ecc.
Cthutu

1
Penso che se lo fai myObject->iVarsi bloccherà, indipendentemente dal fatto che sia C con o senza oggetti. (mi dispiace per la tomba.)
11684

3
@ 11684 È corretto, ma ->non è più un'operazione Objective-C ma piuttosto un C-ism generico.
bbum

1
La recente API di root exploit / backdoor nascosta OSX è accessibile a tutti gli utenti (non solo agli amministratori) grazie alla messaggistica Nil di obj-c.
dcow

51

Un messaggio per nilnon fa nulla e ritorna nil, Nil, NULL, 0, o 0.0.


41

Tutti gli altri post sono corretti, ma forse è il concetto che è la cosa importante qui.

Nelle chiamate al metodo Objective-C, qualsiasi riferimento a un oggetto che può accettare un selettore è un obiettivo valido per quel selettore.

Ciò consente di risparmiare un SACCO di "è l'oggetto di destinazione di tipo X?" codice - fintanto che l'oggetto ricevente implementa il selettore, non fa assolutamente alcuna differenza di che classe sia! nilè un NSObject che accetta qualsiasi selettore - semplicemente non fa nulla. Questo elimina anche un sacco di codice "controlla per zero, non inviare il messaggio se vero". (Il concetto "se lo accetta, lo implementa" è anche ciò che ti permette di creare protocolli , che sono una specie di interfacce Java: una dichiarazione che se una classe implementa i metodi dichiarati, allora è conforme al protocollo.)

La ragione di ciò è eliminare il codice scimmia che non fa altro che mantenere felice il compilatore. Sì, ottieni l'overhead di un'altra chiamata al metodo, ma risparmi il tempo del programmatore , che è una risorsa molto più costosa del tempo della CPU. Inoltre, stai eliminando più codice e più complessità condizionale dalla tua applicazione.

Chiarimento per i downvoters: potresti pensare che questo non sia un buon modo di procedere, ma è il modo in cui il linguaggio è implementato ed è l'idioma di programmazione consigliato in Objective-C (vedi le lezioni di programmazione per iPhone di Stanford).


17

Ciò significa che il runtime non produce un errore quando objc_msgSend viene chiamato sul puntatore nil; invece restituisce un valore (spesso utile). I messaggi che potrebbero avere un effetto collaterale non fanno nulla.

È utile perché la maggior parte dei valori predefiniti è più appropriata di un errore. Per esempio:

[someNullNSArrayReference count] => 0

Cioè, nil sembra essere l'array vuoto. Nascondere un riferimento NSView nullo non fa nulla. Comodo, eh?


12

Nella citazione dalla documentazione, ci sono due concetti separati - forse potrebbe essere meglio se la documentazione lo rendesse più chiaro:

Ci sono diversi modelli in Cocoa che sfruttano questo fatto.

Il valore restituito da un messaggio a nil può anche essere valido:

Il primo è probabilmente più rilevante qui: tipicamente essere in grado di inviare messaggi a nilrende il codice più semplice - non devi controllare i valori nulli ovunque. L'esempio canonico è probabilmente il metodo di accesso:

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

Se l'invio di messaggi a nilnon fosse valido, questo metodo sarebbe più complesso: dovresti avere due controlli aggiuntivi per assicurarti valuee newValuenon lo sono nilprima di inviare loro i messaggi.

L'ultimo punto (che i valori restituiti da un messaggio a nilsono anche tipicamente validi), tuttavia, aggiunge un effetto moltiplicatore al primo. Per esempio:

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

Anche questo codice non richiede un controllo dei nilvalori e scorre naturalmente ...

Detto questo, la flessibilità aggiuntiva a cui essere in grado di inviare messaggi nilha un certo costo. C'è la possibilità che a un certo punto scriverete codice che fallisce in un modo particolare perché non avete tenuto conto della possibilità che un valore possa essere nil.


12

Da Greg Parker 's sito :

Se si esegue LLVM Compiler 3.0 (Xcode 4.2) o successivo

Messaggi a zero con tipo di ritorno | ritorno
Numeri interi fino a 64 bit | 0
Virgola mobile fino al doppio lungo | 0.0
Puntatori | zero
Structs | {0}
Qualsiasi tipo _Complex | {0, 0}

9

Spesso significa non dover controllare la presenza di oggetti nulli ovunque per motivi di sicurezza, in particolare:

[someVariable release];

oppure, come notato, vari metodi di conteggio e lunghezza restituiscono tutti 0 quando hai un valore nullo, quindi non devi aggiungere controlli extra per nil dappertutto:

if ( [myString length] > 0 )

o questo:

return [myArray count]; // say for number of rows in a table

Tieni presente che l'altro lato della medaglia è il potenziale di bug come "if ([myString length] == 1)"
hatfinch

Come è un bug? [myString length] restituisce zero (zero) se myString è nil ... una cosa che penso potrebbe essere un problema è [myView frame] che penso possa darti qualcosa di strano se myView è nullo.
Kendall Helmstetter Gelner

Se progetti le tue classi e metodi intorno al concetto che i valori predefiniti (0, nil, NO) significano "non utile", questo è uno strumento potente. Non devo mai controllare le mie corde per zero prima di controllare la lunghezza. Per me una stringa vuota è inutile e una stringa nulla durante l'elaborazione del testo. Sono anche uno sviluppatore Java e so che i puristi Java lo eviteranno, ma consente di risparmiare un sacco di codice.
Jason Fuerstenberg

6

Non pensare che "il ricevitore sia nullo"; Sono d'accordo, è piuttosto strano. Se stai inviando un messaggio a zero, non c'è nessun destinatario. Stai solo inviando un messaggio a niente.

Come affrontarlo è una differenza filosofica tra Java e Objective-C: in Java, è un errore; in Objective-C, è un no-op.


C'è un'eccezione a questo comportamento in java, se chiami una funzione statica su un null equivale a chiamare la funzione sulla classe in fase di compilazione della variabile (non importa se è nulla).
Roman A. Taycher

6

I messaggi ObjC che vengono inviati a zero e i cui valori di ritorno hanno dimensioni maggiori di sizeof (void *) producono valori indefiniti sui processori PowerPC. Inoltre, questi messaggi provocano la restituzione di valori indefiniti nei campi di strutture la cui dimensione è maggiore di 8 byte anche sui processori Intel. Vincent Gable lo ha descritto bene nel suo post sul blog


6

Non credo che nessuna delle altre risposte lo abbia menzionato chiaramente: se sei abituato a Java, dovresti tenere presente che mentre Objective-C su Mac OS X ha il supporto per la gestione delle eccezioni, è una caratteristica del linguaggio opzionale che può essere attivato / disattivato con un flag del compilatore. La mia ipotesiè che questo progetto di "inviare messaggi a nilè sicuro" precede l'inclusione del supporto per la gestione delle eccezioni nella lingua ed è stato fatto con un obiettivo simile in mente: i metodi possono tornare nilper indicare errori, e poiché l'invio di un messaggio a nilsolitamente restituiscenila sua volta, questo consente all'indicazione di errore di propagarsi attraverso il codice in modo da non doverlo controllare ad ogni singolo messaggio. Devi solo verificarlo nei punti in cui è importante. Personalmente penso che la propagazione e la gestione delle eccezioni sia un modo migliore per affrontare questo obiettivo, ma non tutti potrebbero essere d'accordo con questo. (D'altra parte, ad esempio, non mi piace il requisito di Java di dover dichiarare quali eccezioni può generare un metodo, il che spesso ti costringe a propagare sintatticamente dichiarazioni di eccezioni in tutto il codice; ma questa è un'altra discussione.)

Ho pubblicato una risposta simile, ma più lunga, alla domanda correlata "È necessario affermare che la creazione di ogni oggetto è riuscita nell'Obiettivo C?" se vuoi maggiori dettagli.


Non ci ho mai pensato in quel modo. Questa sembra essere una caratteristica molto conveniente.
mk12

2
Buona intuizione, ma storicamente imprecisa sul motivo per cui è stata presa la decisione. La gestione delle eccezioni esisteva nella lingua sin dall'inizio, sebbene i gestori di eccezioni originali fossero piuttosto primitivi rispetto all'idioma moderno. Nil-mangia-message è stata una scelta progettuale consapevole derivata dal comportamento opzionale dell'oggetto Nil in Smalltalk. Quando sono state progettate le API NeXTSTEP originali, il concatenamento dei metodi era abbastanza comune e un nilritorno spesso utilizzato per cortocircuitare una catena in un NO-op.
bbum

2

C non rappresenta niente come 0 per i valori primitivi e NULL per i puntatori (che è equivalente a 0 in un contesto di puntatore).

Objective-C si basa sulla rappresentazione del nulla di C aggiungendo nil. nil è un puntatore a un oggetto. Sebbene semanticamente distinti da NULL, sono tecnicamente equivalenti l'uno all'altro.

Gli NSObject appena allocati iniziano con il loro contenuto impostato a 0. Ciò significa che tutti i puntatori di quell'oggetto ad altri oggetti iniziano come nil, quindi non è necessario, ad esempio, impostare self. (Association) = nil nei metodi init.

Il comportamento più notevole di nil, tuttavia, è che può ricevere messaggi inviati.

In altri linguaggi, come C ++ (o Java), questo bloccherebbe il tuo programma, ma in Objective-C, invocare un metodo su nil restituisce un valore zero. Ciò semplifica enormemente le espressioni, in quanto evita la necessità di verificare la presenza di zero prima di fare qualsiasi cosa:

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

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

Essere consapevoli di come funziona nil in Objective-C consente a questa comodità di essere una funzionalità e non un bug in agguato nella tua applicazione. Assicurati di proteggerti dai casi in cui i valori nil non sono desiderati, controllando e restituendo in anticipo per non riuscire in modo silenzioso o aggiungendo un NSParameterAssert per generare un'eccezione.

Fonte: http://nshipster.com/nil/ https://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocObjectsClasses.html (Sending Message to nil).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.