Envoi d'un message à nil en Objective-C

107

En tant que développeur Java qui lit la documentation Objective-C 2.0 d'Apple: je me demande ce que signifie « envoyer un message à nil » - sans parler de son utilité. Prenant un extrait de la documentation:

Il existe plusieurs modèles dans Cocoa qui profitent de ce fait. La valeur renvoyée par un message à nil peut également être valide:

  • Si la méthode retourne un objet, tout type de pointeur, tout scalaire entier de taille inférieure ou égale à sizeof (void *), un float, un double, un long double ou un long long, alors un message envoyé à nil renvoie 0 .
  • Si la méthode renvoie une structure, telle que définie par le Guide des appels de fonction ABI de Mac OS X à renvoyer dans les registres, un message envoyé à nil renvoie 0,0 pour chaque champ de la structure de données. Les autres types de données de structure ne seront pas remplis de zéros.
  • Si la méthode renvoie autre chose que les types valeur mentionnés ci-dessus, la valeur de retour d'un message envoyé à nil n'est pas définie.

Java a-t-il rendu mon cerveau incapable de comprendre l'explication ci-dessus? Ou y a-t-il quelque chose qui me manque qui rendrait cela aussi clair que du verre?

J'ai l'idée de messages / récepteurs en Objective-C, je suis simplement confus au sujet d'un récepteur qui se trouve être nil.

Ryan Delucchi
la source
2
J'avais aussi un background Java et j'étais terrifié par cette fonctionnalité intéressante au début, mais maintenant je la trouve absolument MAGNIFIQUE !;
Valentin Radu
1
Merci, c'est une excellente question. En avez-vous vu les avantages? Cela me semble "pas un bug, une fonctionnalité". Je continue à avoir des bogues où Java me giflerait avec une exception, donc je savais où était le problème. Je ne suis pas heureux d'échanger l'exception de pointeur nul pour enregistrer une ligne ou deux de code trivial ici et là.
Maciej Trybiło

Réponses:

92

Eh bien, je pense que cela peut être décrit à l'aide d'un exemple très artificiel. Disons que vous avez une méthode en Java qui imprime tous les éléments d'un ArrayList:

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

Maintenant, si vous appelez cette méthode comme ceci: someObject.foo (NULL); vous allez probablement avoir une NullPointerException quand il essaiera d'accéder à la liste, dans ce cas dans l'appel à list.size (); Maintenant, vous n'appeleriez probablement jamais someObject.foo (NULL) avec la valeur NULL comme ça. Cependant, vous avez peut-être obtenu votre ArrayList à partir d'une méthode qui renvoie NULL si elle rencontre une erreur générant la ArrayList comme someObject.foo (otherObject.getArrayList ());

Bien sûr, vous aurez également des problèmes si vous faites quelque chose comme ceci:

ArrayList list = NULL;
list.size();

Maintenant, en Objective-C, nous avons la méthode équivalente:

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

Maintenant, si nous avons le code suivant:

[someObject foo:nil];

nous avons la même situation dans laquelle Java produira une NullPointerException. L'objet nil sera accédé en premier à [anArray count] Cependant, au lieu de lancer une NullPointerException, Objective-C renverra simplement 0 conformément aux règles ci-dessus, donc la boucle ne fonctionnera pas. Cependant, si nous définissons la boucle pour qu'elle s'exécute un certain nombre de fois, nous envoyons d'abord un message à anArray at [anArray objectAtIndex: i]; Cela retournera également 0, mais puisque objectAtIndex: renvoie un pointeur, et un pointeur sur 0 est nul / NULL, NSLog sera passé nil à chaque fois dans la boucle. (Bien que NSLog soit une fonction et non une méthode, il imprime (null) s'il est passé un NSString nul.

Dans certains cas, il est plus agréable d'avoir une NullPointerException, car vous pouvez dire tout de suite que quelque chose ne va pas avec le programme, mais à moins que vous n'attrapiez l'exception, le programme plantera. (En C, essayer de déréférencer NULL de cette manière provoque le blocage du programme.) En Objective-C, cela provoque simplement un comportement d'exécution éventuellement incorrect. Cependant, si vous avez une méthode qui ne s'arrête pas si elle renvoie 0 / nil / NULL / une structure remise à zéro, cela vous évite d'avoir à vérifier que l'objet ou les paramètres sont nuls.

Michael Buckley
la source
33
Il vaut probablement la peine de mentionner que ce comportement a fait l'objet de nombreux débats dans la communauté Objective-C au cours des deux dernières décennies. Le compromis entre «sécurité» et «commodité» est évalué différemment par différentes personnes.
Mark Bessey
3
En pratique, il y a beaucoup de symétrie entre la transmission de messages à zéro et le fonctionnement d'Objective-C, en particulier dans la nouvelle fonctionnalité de pointeurs faibles d'ARC. Les pointeurs faibles sont automatiquement remis à zéro. Alors concevez votre API pour qu'elle puisse répondre à 0 / nil / NIL / NULL etc.
Cthutu
1
Je pense que si vous le faites, myObject->iVaril plantera, peu importe si c'est C avec ou sans objets. (désolé pour gravedig.)
11684
3
@ 11684 C'est correct, mais ce ->n'est plus une opération Objective-C mais bien un C-isme générique.
bbum
1
Le récent exploit racine OSX / API de porte dérobée cachée est accessible à tous les utilisateurs (pas seulement aux administrateurs) en raison de la messagerie Nil d'obj-c.
dcow le
51

Un message nilne fait rien et retourne nil, Nil, NULL, 0ou 0.0.

Peter Hosey
la source
41

Tous les autres articles sont corrects, mais c'est peut-être le concept qui est important ici.

Dans les appels de méthode Objective-C, toute référence d'objet qui peut accepter un sélecteur est une cible valide pour ce sélecteur.

Cela économise BEAUCOUP de "l'objet cible est-il de type X?" code - tant que l'objet récepteur implémente le sélecteur, il ne fait absolument aucune différence de quelle classe il s'agit! nilest un NSObject qui accepte n'importe quel sélecteur - il ne fait rien. Cela élimine également beaucoup de code "vérifier pour nil, ne pas envoyer le message si vrai". (Le concept "s'il l'accepte, il l'implémente" est aussi ce qui vous permet de créer des protocoles , qui sont un peu comme les interfaces Java: une déclaration selon laquelle si une classe implémente les méthodes indiquées, alors elle est conforme au protocole.)

La raison en est d'éliminer le code singe qui ne fait rien d'autre que de satisfaire le compilateur. Oui, vous obtenez la surcharge d'un appel de méthode supplémentaire, mais vous gagnez du temps au programmeur , qui est une ressource beaucoup plus coûteuse que le temps CPU. De plus, vous éliminez plus de code et plus de complexité conditionnelle de votre application.

Clarification pour les votants négatifs: vous pensez peut-être que ce n'est pas une bonne voie à suivre, mais c'est la manière dont le langage est implémenté, et c'est l'idiome de programmation recommandé en Objective-C (voir les conférences de programmation iPhone de Stanford).

Joe McMahon
la source
17

Cela signifie que le runtime ne produit pas d'erreur lorsque objc_msgSend est appelé sur le pointeur nil; à la place, il renvoie une valeur (souvent utile). Les messages qui pourraient avoir un effet secondaire ne font rien.

C'est utile car la plupart des valeurs par défaut sont plus appropriées qu'une erreur. Par exemple:

[someNullNSArrayReference count] => 0

Ie, nil semble être le tableau vide. Cacher une référence NSView nulle ne fait rien. Pratique, hein?

Riches
la source
12

Dans la citation de la documentation, il y a deux concepts distincts - il serait peut-être préférable que la documentation le précise plus clairement:

Il existe plusieurs modèles dans Cocoa qui profitent de ce fait.

La valeur renvoyée par un message à nil peut également être valide:

Le premier est probablement plus pertinent ici: le fait de pouvoir généralement envoyer des messages à nilrend le code plus simple - vous n'avez pas à vérifier les valeurs nulles partout. L'exemple canonique est probablement la méthode accesseur:

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

Si l'envoi de messages à niln'était pas valide, cette méthode serait plus complexe - vous devrez avoir deux vérifications supplémentaires pour vous assurer valueet newValuene le sont pas nilavant de leur envoyer des messages.

Le dernier point (les valeurs renvoyées par un message à nilsont également généralement valides), cependant, ajoute un effet multiplicateur au premier. Par exemple:

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

Ce code ne nécessite pas de vérification des nilvaleurs et s'écoule naturellement ...

Cela dit, la flexibilité supplémentaire qui permet d'envoyer des messages a un nilcertain coût. Il est possible que vous écriviez à un moment donné du code qui échoue d'une manière particulière parce que vous n'avez pas pris en compte la possibilité qu'une valeur puisse être nil.

mmalc
la source
12

De Greg Parker de » site :

Si vous exécutez LLVM Compiler 3.0 (Xcode 4.2) ou version ultérieure

Messages à nil avec type de retour | revenir
Entiers jusqu'à 64 bits | 0
Virgule flottante jusqu'à long double | 0,0
Pointeurs | néant
Structs | {0}
Tout type _Complex | {0, 0}
Frontières de la santé
la source
9

Cela signifie souvent ne pas avoir à vérifier partout pour la sécurité aucun objet - en particulier:

[someVariable release];

ou, comme indiqué, diverses méthodes de comptage et de longueur renvoient toutes 0 lorsque vous avez une valeur nulle, vous n'avez donc pas à ajouter de vérifications supplémentaires pour nil partout:

if ( [myString length] > 0 )

ou ca:

return [myArray count]; // say for number of rows in a table
Kendall Helmstetter Gelner
la source
Gardez à l'esprit que l'autre côté de la médaille est le potentiel de bogues tels que "if ([myString length] == 1)"
hatfinch
En quoi est-ce un bug? [myString length] renvoie zéro (nil) si myString est nul ... une chose que je pense pourrait être un problème est [myView frame] qui, je pense, peut vous donner quelque chose de farfelu si myView est nul.
Kendall Helmstetter Gelner
Si vous concevez vos classes et méthodes autour du concept que les valeurs par défaut (0, nil, NO) signifient «pas utile», c'est un outil puissant. Je n'ai jamais besoin de vérifier la valeur nulle de mes chaînes avant de vérifier la longueur. Pour moi, une chaîne vide est aussi inutile et une chaîne nulle lorsque je traite du texte. Je suis également un développeur Java et je sais que les puristes Java éviteront cela, mais cela économise beaucoup de codage.
Jason Fuerstenberg
6

Ne pensez pas que "le récepteur est nul"; Je suis d' accord, c'est assez bizarre. Si vous envoyez un message à zéro, il n'y a pas de destinataire. Vous n'envoyez simplement un message à rien.

Comment gérer cela est une différence philosophique entre Java et Objective-C: en Java, c'est une erreur; en Objective-C, c'est un no-op.

benzado
la source
Il y a une exception à ce comportement en java, si vous appelez une fonction statique sur un null, cela équivaut à appeler la fonction sur la classe de compilation de la variable (peu importe si elle est nulle).
Roman A. Taycher
6

Les messages ObjC qui sont envoyés à nil et dont les valeurs de retour ont une taille supérieure à sizeof (void *) produisent des valeurs non définies sur les processeurs PowerPC. En plus de cela, ces messages provoquent le renvoi de valeurs non définies dans les champs des structures dont la taille est supérieure à 8 octets sur les processeurs Intel. Vincent Gable l'a bien décrit dans son article de blog

Nikita Zhuk
la source
6

Je ne pense pas qu'aucune des autres réponses l'ait mentionné clairement: si vous êtes habitué à Java, vous devez garder à l'esprit que si Objective-C sur Mac OS X prend en charge la gestion des exceptions, c'est une fonctionnalité de langage facultative qui peut être activé / désactivé avec un indicateur de compilateur. Je suppose que cette conception de «l'envoi de messages à nilest sûr» est antérieure à l'inclusion de la prise en charge de la gestion des exceptions dans le langage et a été réalisée avec un objectif similaire à l'esprit: les méthodes peuvent retourner nilpour indiquer des erreurs, et depuis l'envoi d'un message, nilrenvoie généralementnilà son tour, cela permet à l'indication d'erreur de se propager dans votre code afin que vous n'ayez pas à la vérifier à chaque message. Vous n'avez qu'à le vérifier aux points où cela compte. Personnellement, je pense que la propagation et la gestion des exceptions sont un meilleur moyen d'atteindre cet objectif, mais tout le monde n'est pas d'accord avec cela. (D'un autre côté, par exemple, je n'aime pas l'exigence de Java selon laquelle vous devez déclarer les exceptions qu'une méthode peut lancer, ce qui vous oblige souvent à propager syntaxiquement des déclarations d'exception dans tout votre code; mais c'est une autre discussion.)

J'ai posté une réponse similaire, mais plus longue, à la question connexe "Est-il nécessaire d'affirmer que chaque création d'objet a réussi en Objective C?" si vous voulez plus de détails.

Rinzwind
la source
Je n'y ai jamais pensé de cette façon. Cela semble être une fonctionnalité très pratique.
mk12
2
Bonne supposition, mais historiquement inexacte sur la raison pour laquelle la décision a été prise. La gestion des exceptions existait dans le langage depuis le début, bien que les gestionnaires d'exceptions d'origine soient assez primitifs par rapport à l'idiome moderne. Nil-eats-message était un choix de conception conscient dérivé du comportement optionnel de l'objet Nil dans Smalltalk. Lorsque les API NeXTSTEP d'origine ont été conçues, le chaînage de méthodes était assez courant et un nilretour souvent utilisé pour court-circuiter une chaîne en un NO-op.
bbum
2

C ne représente rien comme 0 pour les valeurs primitives et NULL pour les pointeurs (ce qui équivaut à 0 dans un contexte de pointeur).

Objective-C s'appuie sur la représentation de rien par C en ajoutant nil. nil est un pointeur d'objet vers rien. Bien que sémantiquement distincts de NULL, ils sont techniquement équivalents les uns aux autres.

Les NSObjects nouvellement alloués démarrent avec leur contenu défini sur 0. Cela signifie que tous les pointeurs de cet objet vers d'autres objets commencent par nil, il est donc inutile, par exemple, de définir self. (Association) = nil dans les méthodes init.

Le comportement le plus notable de nil, cependant, est qu'il peut recevoir des messages.

Dans d'autres langages, comme C ++ (ou Java), cela planterait votre programme, mais en Objective-C, invoquer une méthode sur nil renvoie une valeur nulle. Cela simplifie considérablement les expressions, car cela évite d'avoir à vérifier nil avant de faire quoi que ce soit:

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

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

Être conscient de la façon dont nil fonctionne dans Objective-C permet à cette commodité d'être une fonctionnalité et non un bogue caché dans votre application. Assurez-vous de vous prémunir contre les cas où les valeurs nulles sont indésirables, soit en vérifiant et en retournant tôt pour échouer silencieusement, soit en ajoutant un NSParameterAssert pour lever une exception.

Source: http://nshipster.com/nil/ https://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocObjectsClasses.html (Envoi d'un message à nil).

Zee
la source