Vincent Tourraine
Blog

Nullability et initialisation

#Objective-C #Xcode #dev

Mon dernier billet de blog présentait les nullability annotations, en essayant d’expliquer comment elles contribuent à améliorer un code Objective-C. Avec un peu de recul, et après davantage de temps passé à les mettre en pratique, ces annotations me posent un problème. Un cas particulier, peut-être, mais absolument incontournable, puisqu’il s’agit de l’initialisation des objets.

if (self)

L’implémentation d’un constructeur commence habituellement comme ceci :

-(instancetype)init {
  self = [super init];
  if (self) {
    // …
  }
  return self;
}

Rien de surprenant, il s’agit même du template fourni par Xcode. Pourtant, ce conditionnel au milieu de la méthode soulève une question intéressante :

Que se passe-t-il quand l’initialisation d’un objet échoue ?

Soit la classe parent retourne nil, soit elle émet une exception.

Ce template a justement pour but de gérer ce premier cas, pour empêcher de poursuivre l’initialisation inutilement. Le problème vient alors potentiellement du code qui initialise l’objet sans considérer la possibilité d’un échec. La neutralité d’Objective-C vis-à-vis des variables nulles permet généralement de s’en tirer sans trop de problèmes, mais on voit bien comme les nullability annotations exposent ce cas particulier, et nous encouragent à fournir une solution explicite.

« nonnull » ou « nullable » ?

Pour formuler le problème différemment : la valeur de retour d’un constructeur doit-elle être annotée nonnull ou nullable ?

L’en-tête de NSObject pour le SDK iOS 9 ne tranche pas, mais celui pour UIView, par exemple, fournit un début de réponse :

NS_ASSUME_NONNULL_BEGIN
// (…)
-(instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
-(nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
// (…)
NS_ASSUME_NONNULL_END

Réponse, donc : les deux. Ça paraît tout à fait raisonnable, puisque selon la signification et/ou le contexte des paramètres passés, on peut facilement imaginer les deux situations (pour UIView, le constructeur fonctionne quel que soit le CGRect donné, même avec CGRectZero, mais on ne peut pas en dire autant pour un NSCoder *, dont le contenu est totalement indéterminé au moment de la compilation).

Le cas du nullable est assez banal et inintéressant, mais peut-il vraiment y avoir un constructeur avec retour nonnull ?

Le principe de constructeur désigné devrait en théorie nous donner une réponse. Hélas, init n’est pas marqué comme NS_DESIGNATED_INITIALIZER pour NSObject (et sa valeur n’est pas annotée, ce qui nous laisse dans le flou complet). Le cas de NSObject est d’autant plus incertain que l’en-tête varie pour une même version du SDK entre celui trouvé dans usr/include/objc et celui dans Frameworks/Foundation.framework/Headers. On y découvre même un magnifique #if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER, mais je m’égare.

Si initWithFrame: de UIView est marqué avec nonnull, on peut alors logiquement attendre la même chose de init pour NSObject, le constructeur le plus élémentaire qui soit. Si votre classe hérite de NSObject, alors votre constructeur doit pouvoir déclarer la même chose, assurer un retour nonnull. Dans ce cas, le if (self) {} du template évoqué au début de ce billet n’a plus de sens.

Le point Swift

Je ne connais pas assez Swift pour en tirer des conclusions définitives, mais il semble que l’inférence de types proposée par le langague va dans le même sens :

let foo = NSObject()
print(foo.description)

Aucun problème de compilation, il n’y a donc pas d’optionnel, la variable est supposée nonnull.

Conclusion ou simplification ?

J’aimerais beaucoup que ce soit systématiquement le cas, qu’on puisse ainsi simplifier les classes concernées en éliminant ces conditionnels superflus. Tous les if sont des sources de bugs, et un nonnull catégorique est largement préférable à un nullable indéterminé.

Bien sûr, un échec d’initialisation est un cas très rare. Mais cette petite réflexion a des allures de simplification optimiste. Le compilateur peut donner des indications précieuses, encore faut-il que les suppositions de base soient fondées (la joie des axiomes). Entre les bonnes pratiques éprouvées et les avantages potentiels d’une nouvelle approche plus exigeante, mon code hésite.

Vos remarques et conseils sont, évidemment, les bienvenus.

Liens