[Tuto/Cours] Variables d'instance, propriétés et accesseurs

Suite à une petite discussion avec Ali tout à l'heure, je voulais avoir votre avis sur la façon d'écrire votre code quand vous créez ou modifier un objet.
J'utilise souvent des propriétés qui portent le même nom que mes variables d'instances (je pense que 95% des développeurs font ainsi).
Par exemple, dans mon .h, j'ai la variable d'instance :
et sa propriété :
Jusque là , tout va bien.
Dans mon .m, je crée mon objet, et pour ce, j'ai plusieurs façons de le faire.
Si j'oublie mon accesseur, je dois me taper le retain :
et en plus à moi de gérer les futurs releases ou retain, en fonction de l'utilisation de l'objet dans la suite de mon code.
J'utilise alors mon accesseur (ma propriété). Pour cela j'ai 2 solutions.
1) La plus ancienne :
2) Et la nouvelle, disponible depuis objective-C 2.0 je crois :
Ces 2 façons de faire font évidemment un retain puisqu'elles utilisent les accesseurs dont c'est le boulot (puisque demandé dans la propriété).
Or, pour moi, autant dans la façon ancienne (1) je me dis qu'il fait naturellement un retain. Autant dans la nouvelle (2), ça me choque de ne pas voir ce fameux retain écrit après l'objet autoreleasé (databaseWithPath). Evidemment il ne faut pas le mettre sinon on aura une fuite mémoire (+1), mais c'est juste que ça me fait bizarre de ne pas voir ce retain juste à côté...
A la limite, ça me rassurerait de pouvoir voir directement dans le nom d'une propriété s'il y a un retain ou pas. N'y a-t-il pas une convention de nommage là -dessus ?
J'utilise souvent des propriétés qui portent le même nom que mes variables d'instances (je pense que 95% des développeurs font ainsi).
Par exemple, dans mon .h, j'ai la variable d'instance :
FMDatabase *_dataDB;
et sa propriété :
@property (nonatomic, retain) FMDatabase *_dataDB;
Jusque là , tout va bien.

Dans mon .m, je crée mon objet, et pour ce, j'ai plusieurs façons de le faire.
Si j'oublie mon accesseur, je dois me taper le retain :
_dataDB = [[FMDatabase databaseWithPath:kDATA_PATH] retain];
et en plus à moi de gérer les futurs releases ou retain, en fonction de l'utilisation de l'objet dans la suite de mon code.
J'utilise alors mon accesseur (ma propriété). Pour cela j'ai 2 solutions.
1) La plus ancienne :
[self set_dataDB:[FMDatabase databaseWithPath:kDATA_PATH]];
2) Et la nouvelle, disponible depuis objective-C 2.0 je crois :
self._dataDB = [FMDatabase databaseWithPath:kDATA_PATH];
Ces 2 façons de faire font évidemment un retain puisqu'elles utilisent les accesseurs dont c'est le boulot (puisque demandé dans la propriété).
Or, pour moi, autant dans la façon ancienne (1) je me dis qu'il fait naturellement un retain. Autant dans la nouvelle (2), ça me choque de ne pas voir ce fameux retain écrit après l'objet autoreleasé (databaseWithPath). Evidemment il ne faut pas le mettre sinon on aura une fuite mémoire (+1), mais c'est juste que ça me fait bizarre de ne pas voir ce retain juste à côté...
A la limite, ça me rassurerait de pouvoir voir directement dans le nom d'une propriété s'il y a un retain ou pas. N'y a-t-il pas une convention de nommage là -dessus ?
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
1) Ca me choque toujours autant que tu mettes sans avoir écrit [tt][_dataDB release][/tt] avant. Bon certes, dans certains cas (genre par exemple dans le init ou viewDidLoad ou quoi), tu sais qu'il était nil au moment de cet appel, donc tu peux te dire que c'est inutile de faire un release... Mais bon, ça coûte pas grand chose de le faire quand même, pour plus de cohérence (puisque, comme quand tu codes un setter, il faut faire un release de l'ancienne valeur avant de retainer la nouvelle), puisqu'un message à nil est ignoré, donc faire [tt][_dataDB release][/tt] si [tt]_dataDB[/tt] est nil à ce moment la n'a aucune effet... Par contre si par la faute à pas de bol il n'est pas nil, au moins ça ne fera pas de fuite mémoire.
2) J'ai du mal à comprendre pourquoi ça te choque dans un cas ([tt]self._dataDB = ...[/tt]) et pas dans l'autre ([tt][self set_dataDB:...][/tt]). C'est sans doute que je suis habitué et que moi ça ne me choque plus, j'en sais rien... mais en fait surtout ce que je ne capte pas c'est pourquoi, pour que ça te choque moins, tu voudrais faire un retain car tu dis que ça te manque de pas le voir explicitement... mais que par contre ça ne te choque pas du tout de ne pas avoir de release, alors qu'il est tout aussi important et que j'ai quand même l'impression que tu l'oublies souvent (puisque tu l'as passé sous silence aussi lors de notre discussion MSN...), comme si tu pensais qu'il était secondaire, ou que tu ne le faisais pas systématiquement...
Après, comme je t'ai dit, rien ne t'oblige à nommer tes propriétés et tes variables d'instances de la même manière. D'ailleurs justement moi quand je veux éviter la confusion, j'utilise souvent des noms différents, avec un underscore pour l'ivar, et sans pour la @property.
Pour rappel, déclarer une [tt]@property Type nom[/tt] revient à la même chose que de déclarer le setter [tt]-(void)setNom:(Type)newValue;[/tt] et le getter [tt]-(Type)nom;[/tt].
Bon sauf qu'en plus cela a d'autres avantages, comme l'introspection de type, ou le fait que le code pour ces setters et getters peut si l'on veut être généré automatiquement via @synthesize, bien que ça ne soit pas une obligation, ainsi aussi que l'indication de la politique d'accès mémoire, comme (retain) ou (assign) ou (copy). Mais sinon principalement ça déclare ces setters et getters.
- Donc d'une part ces setters ou getters vont faire le retain et release à notre place s'ils sont bien codés et sont bien sensés retenir l'objet et non pas juste l'assigner (comme c'est le cas pour les delegate en general, ou évidemment pour les variables de type non-objet comme les int ou float)
- Et aussi on peut tout à fait donner à ces setters ou getters des noms différents des variables d'instances que l'on veut associer
- On peut même ne pas associer du tout de variable d'instance à ces @property, oui oui, ce n'est pas obligatoire non plus. Ce sont juste des méthodes setNom: et nom, mais qui peuvent en réalité par exemple envoyer une requête réseau pourquoi pas, ou aller lire dans les UserDefaults, ou stocker cette valeur dans un autre objet (NSDictionary, ...), ou que sais-je encore.
Je dis ça parce que beaucoup (moi le premier au début avant que je comprenne bien tout ça) mettent dans le même panier @property, @synthesize, et la "syntaxe pointée" (self.machin) d'Objective-C 2.0, alors que les 3 peuvent être utilisés indépendamment.
(c'est un peu dense comme post mais parce que j'explique chacun des 5 cas illustrés pour montrer les différences) :
- Dans ce cas, on peut modifier la variable d'instance directement par [tt]name = @toto;[/tt], mais cela ne fera aucun retain ni release bien sûr, donc mènera dans le cas présenté en généralà une fuite mémoire (sauf si on sait ce qu'on fait et qu'on s'assure d'avoir bien fait les release nécessaires puis les retains nécessaires aussi, ..)
- Ou on peut utiliser le setter explicitement, avec [self setName:@toto], qui lui, puisqu'on utilise le setter, fera bien un release de l'ancienne valeur et un retain de la nouvelle, tout ça de façon transparente, c'est un setter, c'est son rôle.
- Ou alors on peut utiliser aussi la nouvelle notation pointée d'Objective-C 2.0, [tt]self.name=@toto[/tt], qui est strictement équivalente à la précédente [tt][self setName:@toto][/tt] (en réalité la notation pointée est convertie en appel au setter ou au getter au moment de la compilation, et non au runtime, d'ailleurs). Mais il ne faut du coup pas confondre [tt]self.name = @toto[/tt] et [tt]name = @toto;[/tt], le premier faisant appel au setter, le second faisant une bête affectation de variable d'instance, donc avec potentiels leaks.
- le IBOutlet étant devant la ivar, dans IB on va se voir proposer un outlet nommé "texLabel", du nom de l'ivar devant laquelle on a mis le mot clé
- cette fois on ne demande pas au compilo de générer le code des méthodes text et setText tout seul, mais on les implémente nous-même (3c, 3d), de sorte que l'appel à ces méthodes modifie le texte du UILabel.
- Ainsi, on peut indifféremment appeler [tt][self setText:@toto][/tt] ou [tt]self.text=@toto[/tt] (ces deux syntaxes étant exactement équivalentes) pour modifier le texte du textLabel.
- On peut aussi si l'on veut (mais uniquement depuis le code de la classe évidemment pour avoir accès à ses ivars), écrire textLabel.text = @toto (mais on ne pourra pas faire ça depuis une autre classe que la classe Dummy, autre classe qui n'aura pas accès à l'ivar textLabel, mais pourra appeller setText par contre sur un objet Dummy)
Par contre cette fois, le mot clé "IBOutlet" est sur la @property nommée activityView, ce qui permet de présenter dans IB l'outlet sous ce nom. Si on avait mis le mot clé IBOutlet sur l'ivar _activityView, on aurait vu comme nom pour l'outlet dans IB "_activityView" et non "activityView", ce qui est quand même moins joli :P
En plus, puisqu'il existe un setter associé, lors du désarchivage du NIB, c'est le setter qui va être appelé pour affecter le contrôle UIActivityIndicatorView à cette variable _activityView, via justement ce setter setActivityView: désigné par la @property...
Mais rien n'empêche de déclarer cette @property comme je l'ai fait en (5b) et (5c), même si elle n'a ni variable d'instance associée ni que son code est généré automatiquement via @synthesize, mais qu'on écrit le code nous-même (en l'occurence ça pilote l'activityView comme vous pouvez le voir ou retourne son état).
De plus, dans ce cas 5, on voit que j'ai redéfini le nom du getter associé à cette @property, c'est quand même plus sympa d'avoir un getter nommé "isLoading" que juste "loading" pour avoir l'état
Ce qu'il faut retenir de tous ces exemples :
- @property est souvent lié à une ivar mais ce n'est pas obligatoire
- si c'est le cas, l'ivar n'est pas obligée d'avoir le même nom que la @property, ce qui peut éclaircir les choses au début pour mettre en avant la différence entre l'ivar et la propriété si vous n'y voyez pas clair
- utiliser @property n'implique pas forcément d'utiliser @synthesize, on peut aussi coder nos accesseurs nous-mêmes
- la syntaxe pointée est indépendante des @property
- la syntaxe pointée est exactement équivalente à appeler le setter ([tt]self.machin = truc[/tt] vaut [tt][self setMachin:truc][/tt]) ou le getter ([tt]truc = self.machin[/tt] vaut [tt]truc = [self machin][/tt])
- ne pas confondre l'accès direct à une ivar (_dbPath) avec la syntaxe pointée pour appeler une @property (self.dbPath), en particulier l'affectation d'une ivar comme [tt]dbPath=@toto.db[/tt] ne fait pas de release/retain, mais juste une affectation, alors que la syntaxe pointée [tt]self.dbPath=@toto.db[/tt], étant exactement équivalente à [tt][self setDbPath:@toto.db][/tt], fait bien appel au setter associé à la variable, qui donc se charge de faire le release de la variable précédente et le retain de la nouvelle
Au final, si tu accèdes directement à l'ivar, ça fait une affectation, donc aucun appel à un quelconque release ni retain, et si tu utilises les @property(retain), que ce soit via la syntaxe pointée self.dbPath=... ou par l'appel direct à setDbPath:..., ça fait le release et ça aussi le retain, comme ça a toujours été le sensé être le cas pour tout setter bien codé voulant utiliser la politique du retain/release avec sa variable.
Je ne savais pas qu'on pouvait faire autant de choses avec les propriétés, et que leur nom pouvait se détacher complétement de celui de la variable d'instance déclarée qu'ils retain/release.
Pour les outlets, je m'étais souvent demandé pourquoi ils étaient parfois déclarés avec l'ivar et parfois avec la propriété. Maintenant je sais !
Je pense que je vais m'habituer à la syntaxe pointée, mais sans changer le nom des accesseurs (sans underscore). J'aime quand même bien voir mon undescore, je sais qu'il ne s'agit ainsi pas de variables de méthodes.
Super merci en tout cas, je vais m'autoriser plus de liberté maintenant.
EDIT : à vrai dire, des choses paraissaient plus évidentes et compréhensibles avant l'introduction des @property et @synthesize. On devait coder les accesseurs en dur. Mais pour rien au monde je voudrais y revenir puisque c'est un gain de temps conséquent, sauf dans le cas où l'on doit les customizer comme tu l'as montré.
Ainsi, j'ai bien mes 4 ivars déclarées avec underscore, et pour 2 d'entre elles j'ai un outlet donc je renomme l'accesseur sans undescore et attribue la variable d'instance avec undescore.
Côté IB, j'ai bien mes outlets sans undescore, et dans la suite de mon code, je me sers de la syntaxe pointée.
EDIT : En fait j'ai un doute. Est-ce qu'il faut mieux que j'écrive du coup :
ou
iVar c'est bien variable d'instance ?
edit : oui
Et l'underscore devant le nom il a une utilité particulière ou bien il sert juste pour "repère", est-il prit en compte ou ignoré par le compilateur ?
edit: je crois comprendre (exemple 2) qu'il te sert juste de repère (on pourrait mettre iVarDbPath à la place de _dbPath) du moment que l'on écrive:
@synthesize dbPath= iVarDbPath;
Je vais relire car c'est un peu hard a avaler des le matin.
Encore un grand MERCI.
Peu importe qu'on renomme l'accès à cette ivar dans une déclaration @property histoire de virer le undescore. Effectivement ça permet bien de distinguer rapidement le type d'accès, direct ou par setter, qu'on y fait. Mais ça rend implicitement cette ivar directement accessible de l'extérieur.
L'undescore est une convention de nommage privée ? Ce n'est pas juste une convention de nommage des ivars pour les différencier des vars de méthode ?
J'ai toujours entendu ou lu :
- pas d'undescore => variable de méthode
- 1 undescore => variable d'instance
- 2 undescores => variable de classe
Mais tu as raison, Apple, dans ses headers, déclare toutes les variables d'instances de ses classe en @private et elles sont toutes "underscorées".
Perso, je trouve que de mettre un underscore à toutes les variables d'instances de nos classes n'a pas de sens et ne permet plus de distinguer quoique ce soit.
Autant n'en mettre aucun.
Et c'est du reste ce qu'Apple nous incite à faire dans la citation que j'ai mise au dessus.
C'est aussi ainsi qu'ils font tout au long des exemples de code qu'il nous propose du reste ... (au moins c'est plus facile à écrire et à lire)
Perso, j'aime ni le tout ni le rien, et je trouve logique de se servir du underscore pour qu'on distingue du premier coup d'oeil une simple variable d'instance d'une variable au comportement privé nécessitant un traitement spécial:
Par exemple, une ivar underscorée serait plutôt à mon sens de ce type:
Dans le .h
Dans limplémentation:
Là ce n'est plus un "bête" setter et pas question qu'un autre objet modifie l'ivar _simplisticPreferences seulement sans que l'AppDelegate n'ai modifié l'interface en conséquence ...
Une question d'habitude et/ou de goûts peut-être ?
[EDIT] je me rends compte que mon exemple laisse penser que [tt]-setSimplistic[/tt] est un setter alors que le plus souvent ici ce serait plutot une IBAction genre [tt]-(void) togglePrefMode:(id) sender[/tt] (écrite différemment du reste...) qui ferait changer la valeur de [tt] _simplisticPreferences[/tt] ...
Le résultat sera le même mais dans le deuxième cas tu ne profiteras pas des effets de la synthetisation de ta propriété (implementation KVC, synchronisation si atomic, etc...).
Une autre solution pour rendre les accesseurs des variables d'instance plus commodes à lire est d'utiliser les options de renommage des propriétés :
Ceci est très utile pour les ivar de type booléen...
Les conventions de code sont très diverses et dépendent fortement du langage pour lequel elles ont été pensées et des programmeurs qui les ont elaboré. Alors pour un même langage on aura souvent plusieurs conventions (et souvent même de belles querelles entre les membres qui les defendent). Ainsi rien ne t'empêche d'en établir une que tu respectes en veillant à ce qu'elle rende le code plus lisible et non le contraire
Oui, je savais que le résultat serait le même dans ce cas
En réfléchissant à ta phrase je me rend compte que les accesseurs générés automatiquement et KVO compliants sont très puissants et peuvent rendre le code d'implémentation d'une classe plus propre et lisible mais introduisent en même temps une sorte de confusion possible dans le code, voire une faille dans la sacro-sainte encapsulation.
Si le @synthétize est fort commode et proprement codé, leur côté KVO compliant augmente encore l'exposition des variables d'instance à l'extérieur de la classe. Ils autorisent, en particuliers, un binding sur ces variables permettant du coup de les modifier directement de façon parfois imprévue à l'origine quand on a implémenté la classe.
Ainsi des accesseurs que l'on aura mis en place pour faciliter le code de la classe peuvent, s'avérer d'une portée inattendue.
Peut-être qu'il faudrait un @ synthétize (private) générant des accesseurs non KVO pour usage exclusif de la classe permettant juste à la classe d'acceder proprement à ses ivar sans pour autant les exposer au reste de l'appli ...? ? ?
Dans un environnement orienté objet c'est conseillé, l'encapsulation étant un principe clé de la POO. Après avec l'objective-C v2 on peut choisir entre la notation traditionnelle (crochetée) ou la notation pointée. Mais dans tous les cas il vaut mieux éviter d'accéder directement aux variables d'instances.
L'implémentation des propriétés en objective-C 2 permet de réaliser assez finement ses choix de conception. On peut par exemple definir une propriété readonly dans l'interface publique d'une classe et la redefinir en readwrite dans une extension de cette même classe (interface privée) :
interface :
Implementation :
Ainsi tu auras un getter public et un setter privé.
On peut encore réécrire le corps des setter/getter en utilisant le mot clé @dynamic en lieu et place de @synthesize.
J'utilise @synthesize et je surcharge le setter. Le problème c'est que le @synthesize génère déjà un setter, que je vais redéfinir. Est-ce qu'il y a une meilleure méthode ?
Hervé, je n'ai rien vu pour indiquer aux propriétés d'être privées, mais cependant n'est-ce pas plutôt aux variables d'instances d'être privées plutôt qu'aux propriétés modifiant ces mêmes variables d'instance ?
Sinon, comme le dit dilaroga, le fait que tu puisses mettre des propriétés en readonly et les setter en interface privée protège pas mal...
Comme ça :
ce qui permet d'écrire son propre setter sans que soit généré automatiquement le setter dynamique.
Par exemple :
Bonne question, je n'ai encore jamais essayé mais en definissant une propriété en readonly et en implémentant une methode correspondant à son setter...
Non, le setter va être implicitement généré mais c'est le nom de la méthode pour l'appeler qui va être modifié.
[EDIT] :
@thibaut
Merci !
Merci, je ne l'avais pas compris dans ce sens !
Tu as raison, c'est bien des ivar privée que je parle.
Mais si, meme en changeant le nom de la propriété, il en existe une qui y accède directement ça peut être un problème.
En fait c'est très valable si tu veux, par exemple, juste assurer la pérénité d'une classe avec dans l'idée qu'au besoin dans l'avenir tu peux changer le nom de l'ivar ou son type et modifiera le setter en conséquence.
L'ivar reste bien privée, le setter publique, et tu te charge du reste.
Par contre ce n'est plus valable si tu définis les propriété par commodité pour le code de la classe mais que tu veux véritablement qu'aucune classe extérieure n'accède aux getter/setter.
Es-tu sûr que de cette façon le setter ne sera pas accessible de l'extérieur ?
Il me semble qu'il sera simplement caché aux yeux de ceux jettant un oeil au Header, auquel cas:
Même si tu n'as pas de setter, dans ce cas rien ne t'empêche de toute façon de faire [tt][monObject setValue:@toto forKey:@privateIVarWithoutSetter][/tt].
Sauf si en plus de ne pas mettre de setter, tu surcharges +accessInstanceVariablesDirectly pour qu'il retourne NO...
Sinon, dans tous les cas, à partir du moment où tu déclares une méthode qui pourra modifier ta variable, même si elle est que dans le .m, si tu connais son nom tu pourras toujours l'appeler, même si c'est dans une extension privée (utiliser [tt]@interface MaClasse()[/tt] dans le .m pour définir des méthodes privées cachées du .h) : donc tu pourras appeler directement la méthode du moment que tu connais son nom, ou si tu veux éviter les warnings, utiliser l'aspect dynamique d'Objective-C et par exemple [tt][toto performSelector:@selector(privateSetter
D'autre part, sur un projet solo est-ce utile ? Où bien est-ce juste une sécurité par rapport à nos propres erreurs possibles ? (je penche pour le cas 2)
Sinon pour quand on est dans la même classe, ne définir le setter que dans une interface privée, ça ne m'est encore jamais arrivé d'en avoir besoin.
Ah, si, pour prévoir des stubs (pour bouchonner mon code), une @property qui n'était pas sensée être readwrite dans le cas d'utilisation normale, car les données étaient téléchargées automatiquement par la classe elle-même, et les variables d'instances affectées en interne... Mais pour les stubs pour bouchonner le temps que le serveur soit prêt, j'avais une classe (que j'incluais dans le target ou non selon mode ouchonné ou pas) qui me rajoutais ce setter utile pour "tricher" pour le cas bouchonné...
Mais bon, c'est encore un cas un peu alambiqué qu'on ne rencontre pas tous les jours ;D
Mais dans ce cas, sans être capilotracté, le mieux me semble de carrément ne pas déclarer d'ivar et de mettre la property dans une extension privée.
Le modern-RunTime se chargera alors de générer l'ivar ni vu ni connu
Mais comme je le disais c'est pas des bidouilleurs que je me méfie mais plus des maladresses dues à une inconscience de la portée des property et des accesseurs. (comme dans mon exemple de binding)
C'est pour ça que si une iVar doit véritablement être protégée, il vaut mieux se passer de déclarer une property liée "juste" pour faciliter le codage des méthodes propres à la classe.
C'est possible ça ?
C'est ici