En Objective-C, pourquoi devrais-je vérifier si self = [super init] n'est pas nul?

165

J'ai une question générale sur l'écriture de méthodes init en Objective-C.

Je vois partout (code Apple, livres, code open source, etc.) qu'une méthode init doit vérifier si self = [super init] n'est pas nul avant de continuer l'initialisation.

Le modèle Apple par défaut pour une méthode init est:

- (id) init
{
    self = [super init];

    if (self != nil)
    {
        // your code here
    }

    return self;
}

Pourquoi?

Je veux dire quand init retournera-t-il jamais nul? Si j'appelle init sur NSObject et que j'obtiens nul, alors quelque chose doit être vraiment foutu, non? Et dans ce cas, autant ne pas écrire de programme ...

Est-il vraiment si courant qu'une méthode init de classe puisse retourner nil? Si oui, dans quel cas et pourquoi?

Jasarien
la source
1
Wil Shipley a publié un article à ce sujet il y a quelque temps. [self = [stupid init];] ( wilshipley.com/blog/2005/07/self-stupid-init.html ) Lisez également les commentaires, quelques bonnes choses.
Ryan Townshend
6
Vous pouvez demander à Wil Shipley ou Mike Ash ou Matt Gallagher . Quoi qu'il en soit, c'est un sujet débattu. Mais en général, il est bon de s'en tenir aux idiomes d'Apple ... ce sont leurs cadres, après tout.
jbrennan
1
Il semble que Wil faisait davantage valoir de ne pas se réaffecter aveuglément pendant l'initialisation, sachant que [super init] pourrait ne pas renvoyer le récepteur.
Jasarien
3
Wil a changé d'avis depuis la publication de ce message.
bbum
5
J'avais vu cette question il y a quelque temps et je l'ai juste retrouvée. Parfait. +1
Dan Rosenstark

Réponses:

53

Par exemple:

[[NSData alloc] initWithContentsOfFile:@"this/path/doesn't/exist/"];
[[NSImage alloc] initWithContentsOfFile:@"unsupportedFormat.sjt"];
[NSImage imageNamed:@"AnImageThatIsntInTheImageCache"];

... etc. (Remarque: NSData peut lever une exception si le fichier n'existe pas). Il existe de nombreux domaines dans lesquels le retour de zéro est le comportement attendu lorsqu'un problème survient, et pour cette raison, il est de pratique courante de vérifier pratiquement tout le temps la valeur de zéro, par souci de cohérence.

iKenndac
la source
10
Oui, mais ce n'est pas À L'INTÉRIEUR de la méthode init de la classe respective. NSData hérite de NSObject. NSData vérifie-t-il si [super init] renvoie nil? C'est ce que je demande ici. Désolé si je n'étais pas clair ...
Jasarien
9
Si vous deviez sous-classer ces classes, il y aurait de fortes chances que [super init] retourne nil. Toutes les classes ne sont pas des sous-classes directes de NSObject. Il n'y a jamais aucune garantie qu'il ne retournera pas nul. C'est juste une pratique de codage défensif qui est généralement encouragée.
Chuck
7
Je doute que NSObject init puisse retourner nil dans tous les cas, jamais. Si vous manquez de mémoire, l'allocation échouera, mais si elle réussit, je doute que init puisse échouer - NSObject n'a même pas de variables d'instance sauf Class. Dans GNUStep, il est implémenté simplement comme "return self", et le démontage sur le Mac a la même apparence. Tout cela est, bien sûr, inévitable - suivez simplement l'idiome standard et vous n'aurez pas à vous soucier de savoir si cela peut ou non.
Peter N Lewis
9
Je ne suis pas déterminé à ne pas suivre les meilleures pratiques. J'aimerais toutefois savoir pourquoi ce sont des pratiques exemplaires en premier lieu. C'est comme se faire dire de sauter d'une tour. Vous ne vous contentez pas de le faire à moins que vous ne sachiez pourquoi. Y a-t-il un gros oreiller moelleux sur lequel atterrir en bas, avec une énorme récompense en espèces? Si je savais que je sauterais. Sinon, je ne le ferais pas. Je ne veux pas simplement suivre aveuglément une pratique sans savoir pourquoi je la suis ...
Jasarien
4
Si alloc retourne nil, init est envoyé à nil, ce qui aboutira toujours à nil, ce qui aboutit à self étant nul.
T.
50

Cet idiome particulier est standard car il fonctionne dans tous les cas.

Bien que rare, il y aura des cas où ...

[super init];

... renvoie une instance différente, nécessitant ainsi l'assignation à soi.

Et il y aura des cas où il renverra nil, nécessitant ainsi la vérification de nil pour que votre code n'essaye pas d'initialiser un emplacement de variable d'instance qui n'existe plus.

L'essentiel est que c'est le modèle correct documenté à utiliser et, si vous ne l'utilisez pas, vous le faites mal.

bbum
la source
3
Est-ce toujours vrai à la lumière des spécificateurs de nullabilité? Si l'initialiseur de ma superclasse est non nul, cela vaut-il vraiment la peine de vérifier? (Bien que NSObject lui-même ne semble pas en avoir pour son -initafaict…)
natevw
Y a-t-il déjà un cas où [super init]renvoie nul lorsque la superclasse directe est NSObject? N'est-ce pas un cas où «tout est cassé»?
Dan Rosenstark le
1
@DanRosenstark Pas si NSObjectc'est la superclasse directe. Mais ... même si vous déclarez NSObjectcomme superclasse directe, quelque chose aurait pu être modifié à l 'exécution de sorte que l NSObject' implémentation de initne soit pas ce que l 'on appelle en réalité.
bbum
1
Merci beaucoup @bbum, cela m'a vraiment aidé à m'orienter dans la résolution de bogues. Bon pour écarter certaines choses!
Dan Rosenstark le
26

Je pense que, dans la plupart des classes, si la valeur de retour de [super init] est nulle et que vous la vérifiez, comme recommandé par les pratiques standard, puis que vous la renvoyez prématurément si elle est nulle, en gros, votre application ne fonctionnera toujours pas correctement. Si vous pensez à ce sujet, même si que si (auto! = Nil) chèque est là, pour le bon fonctionnement de votre classe, 99,99% du temps que vous avez réellement faire besoin d'être auto non nul. Maintenant, supposons que, pour une raison quelconque, [super init] ait renvoyé nil, fondamentalement votre vérification contre nil consiste essentiellement à passer la balle à l'appelant de votre classe, où il échouerait probablement de toute façon, car il supposera naturellement que l'appel était réussi.

En gros, ce à quoi je veux en venir, c'est que 99,99% du temps, le if (self! = Nil) ne vous achète rien en termes de plus grande robustesse, puisque vous ne faites que passer la responsabilité à votre invocateur. Pour vraiment être en mesure de gérer cela de manière robuste, vous devez en fait mettre des vérifications dans toute votre hiérarchie d'appel. Et même dans ce cas, la seule chose que cela vous achèterait, c'est que votre application échouerait un peu plus proprement / solidement. Mais cela échouerait toujours.

Si une classe de bibliothèque décide arbitrairement de retourner nil à la suite d'un [super init], vous êtes de toute façon f *** ed, et c'est plus une indication que l'auteur de la classe de bibliothèque a fait une erreur d'implémentation.

Je pense que c'est plus une suggestion de codage héritée, lorsque les applications s'exécutaient dans une mémoire beaucoup plus limitée.

Mais pour le code de niveau C, je vérifierais toujours généralement la valeur de retour de malloc () par rapport à un pointeur NULL. Alors que, pour Objective-C, jusqu'à ce que je trouve des preuves du contraire, je pense que je vais généralement sauter les vérifications if (self! = Nil). Pourquoi cet écart?

Car, aux niveaux C et malloc, dans certains cas, vous pouvez en fait récupérer partiellement. Alors que je pense qu'en Objective-C, dans 99,99% des cas, si [super init] retourne nil, vous êtes fondamentalement f *** ed, même si vous essayez de le gérer. Vous pouvez tout aussi bien laisser l'application planter et gérer les conséquences.

Gino
la source
6
Bien parlé. Je suis d'accord.
Roger CS Wernersson
3
+1 Je suis totalement d'accord. Juste une petite note: je ne crois pas que le modèle soit le résultat de périodes où l'allocation a échoué le plus souvent. L'allocation est généralement déjà effectuée au moment où init est appelé. Si alloc échouait, init ne serait même pas appelé.
Nikolai Ruhe
8

C'est une sorte de résumé des commentaires ci-dessus.

Disons que la superclasse revient nil. Qu'est-ce qui va arriver?

Si vous ne suivez pas les conventions

Votre code va planter au milieu de votre initméthode. (sauf si initrien n'a d'importance)

Si vous suivez les conventions, ne sachant pas que la superclasse peut retourner nil (la plupart des gens se retrouvent ici)

Votre code va probablement planter plus tard, car votre instance se trouve nillà où vous vous attendiez à quelque chose de différent. Ou votre programme va se comporter de manière inattendue sans planter. Oh cher! Voulez-vous cela? Je ne sais pas...

Si vous suivez les conventions, autorisez volontairement votre sous-classe à renvoyer nil

Votre documentation de code (!) Doit clairement indiquer: "retourne ... ou nil", et le reste de votre code doit être préparé pour gérer cela. Maintenant, cela a du sens.

user123444555621
la source
5
Le point intéressant ici, je pense, est que l'option n ° 1 est clairement préférable à l'option n ° 2. S'il y a de véritables circonstances dans lesquelles vous voudrez peut-être que init de votre sous-classe renvoie nil, alors le # 3 doit être préféré. Si la seule raison pour laquelle cela se produirait est à cause d'un bogue dans votre code, utilisez # 1. L'utilisation de l'option n ° 2 retarde simplement l'explosion de votre application à un moment ultérieur, ce qui rend votre travail beaucoup plus difficile lorsque vous venez de déboguer l'erreur. C'est comme attraper silencieusement des exceptions et continuer sans les gérer.
Mark Amery
Ou vous passez à Swift et utilisez simplement des options
AndrewSB
7

En règle générale, si votre classe dérive directement de NSObject, vous n'en aurez pas besoin. Cependant, c'est une bonne habitude à prendre, car si votre classe dérive d'autres classes, leurs initialiseurs peuvent revenir nil, et si c'est le cas, votre initialiseur peut alors capturer cela et se comporter correctement.

Et oui, pour mémoire, je suis les meilleures pratiques et je l'écris sur toutes mes classes, même celles qui en découlent directement NSObject.

John Rudy
la source
1
Dans cet esprit, serait-il une bonne pratique de vérifier nil après l'initialisation d'une variable et avant d'appeler des fonctions sur celle-ci? par exempleFoo *bar = [[Foo alloc] init]; if (bar) {[bar doStuff];}
dev_does_software
Hériter de NSObjectne garantit pas que cela -initvous donne NSObjectaussi, si vous comptez des runtimes exotiques comme les anciennes versions de GNUstep dans (qui retourne GSObject) donc quoi qu'il arrive, une vérification et une assignation.
Maxthon Chan
3

Vous avez raison, vous pouvez souvent simplement écrire [super init], mais cela ne fonctionnerait pas pour une sous-classe de n'importe quoi. Les gens préfèrent simplement mémoriser une ligne de code standard et l'utiliser tout le temps, même lorsque cela n'est que parfois nécessaire, et ainsi nous obtenons le standard if (self = [super init]), ce qui prend à la fois la possibilité de renvoyer nul et la possibilité d'un objet autre que celui d' selfêtre retourné en compte.

andyvn22
la source
3

Une erreur courante est d'écrire

self = [[super alloc] init];

qui renvoie une instance de la superclasse, ce qui n'est PAS ce que vous voulez dans un constructeur de sous-classe / init. Vous récupérez un objet qui ne répond pas aux méthodes de la sous-classe, ce qui peut prêter à confusion et générer des erreurs déroutantes sur le fait de ne pas repondre à des méthodes ou des identifiants non trouvés, etc.

self = [super init]; 

est nécessaire si la super classe a des membres (variables ou d' autres objets) pour initialiser la première avant la mise en place des membres des sous - classes. Sinon, le runtime objc les initialise tous à 0 ou à nil . ( contrairement à ANSI C, qui alloue souvent des morceaux de mémoire sans les effacer du tout )

Et oui, l'initialisation de la classe de base peut échouer en raison d'erreurs de mémoire insuffisante, de composants manquants, d'échecs d'acquisition de ressources, etc. donc une vérification de zéro est judicieuse et prend moins de quelques millisecondes.

Chris Reid
la source
2

C'est pour vérifier que l'intialazation a fonctionné, l'instruction if renvoie true si la méthode init n'a pas retourné nil, c'est donc un moyen de vérifier que la création de l'objet a fonctionné correctement. Peu de raisons pour lesquelles je peux penser que init pourrait échouer peut-être que c'est une méthode d'initialisation surchargée que la super classe ne connaît pas ou quelque chose du genre, je ne pense pas que ce soit si courant. Mais si cela arrive, il vaut mieux que rien n'arrive qu'un crash que je suppose donc c'est toujours vérifié ...

Daniel
la source
il est, mais htey sont appelés ensemble, que se passe-t-il si alloc f ails?
Daniel
J'imagine que si alloc échoue, alors init sera envoyé à nil plutôt qu'à une instance de la classe sur laquelle vous appelez init. Dans ce cas, rien ne se passera, et aucun code ne sera exécuté pour tester même si [super init] a renvoyé nil ou non.
Jasarien
2
L'allocation de mémoire n'est pas toujours effectuée dans + alloc. Prenons le cas des clusters de classes; une NSString ne sait pas quelle sous-classe spécifique utiliser jusqu'à ce que l'initialiseur soit appelé.
bbum
1

Sous OS X, il est peu probable que l' -[NSObject init]échec soit dû à des raisons de mémoire. On ne peut pas en dire autant pour iOS.

De plus, c'est une bonne pratique pour écrire lors du sous-classement d'une classe qui pourrait revenir nilpour une raison quelconque.

MaddTheSane
la source
2
Sur iOS et Mac OS, il -[NSObject init]est très peu probable qu'il échoue pour des raisons de mémoire car il n'alloue aucune mémoire.
Nikolai Ruhe
Je suppose qu'il voulait dire alloc, pas init :)
Thomas Tempelmann