Gestion mémoire to-one relationShip
Mick
Membre
Bonjour à tous,
c'est sûrement une question très nulle, mais j'avoue avoir un peu de mal avec ça :
J'ai un objet qui a une variable d'instance qui est un pointeur vers un autre objet (to-one relationship). Mon soucis est le suivant :
lorsque qu'un nouveau document est créé, cette relation n'est pas initialisée. selectedObject d'un popup est bindé sur cette relation : le popup indique alors "No Value", ce qui est bien.
Maintenant, si j'ouvre un fichier, dans la méthode initWithCoder, j'initialise via un [[decoder decodeObjectForKey:@clef] retain] => Le retain count est donc à 1... Mais alors, comment écrire l'accesseur ? Si j'écris un accesseur "classique" je doit releaser l'objet avant de l'écraser avec le nouveau que je retient. Mais si celui-ci n'est pas initialisé, (cas d'un nouveau fichier), je release "du vent" lors de l'accès au setter ? cela devrait crasher ? De même, dans le dealloc, je suis sensé releaser ou pas ? Dans le cas de l'ouverture, le retainCount est sensé être à 1, mais dans le cas d'un nouveau fichier, il est sensé être à zéro ? Ou bien interface builder alloue un objet provisoire si la relation n'est pas allouée ?
Bref, si vous avez une explication ...
c'est sûrement une question très nulle, mais j'avoue avoir un peu de mal avec ça :
J'ai un objet qui a une variable d'instance qui est un pointeur vers un autre objet (to-one relationship). Mon soucis est le suivant :
lorsque qu'un nouveau document est créé, cette relation n'est pas initialisée. selectedObject d'un popup est bindé sur cette relation : le popup indique alors "No Value", ce qui est bien.
Maintenant, si j'ouvre un fichier, dans la méthode initWithCoder, j'initialise via un [[decoder decodeObjectForKey:@clef] retain] => Le retain count est donc à 1... Mais alors, comment écrire l'accesseur ? Si j'écris un accesseur "classique" je doit releaser l'objet avant de l'écraser avec le nouveau que je retient. Mais si celui-ci n'est pas initialisé, (cas d'un nouveau fichier), je release "du vent" lors de l'accès au setter ? cela devrait crasher ? De même, dans le dealloc, je suis sensé releaser ou pas ? Dans le cas de l'ouverture, le retainCount est sensé être à 1, mais dans le cas d'un nouveau fichier, il est sensé être à zéro ? Ou bien interface builder alloue un objet provisoire si la relation n'est pas allouée ?
Bref, si vous avez une explication ...
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Voilà comment réécrire getter/setter pout ton objet "clef" (mais tout dépend de comment tu veux qu'il agisse.. retain.. copy.. assign. Vu ton code, ça sera du retain)
j'ai bcq de mal avec ce code ;-)
je peux encore le comprendre dans le cas d'une application multithreadée, encore faut t'il les NSLock ou @synchronized pour garantir le code.
Pour toutes les propriétés "valeurs" qui ne sont pas des objets : on ne fait qu'une assignation simple dans le setter.
Pour toutes les propriétés "valeurs" qui sont des objets (NSString, NSData, NSNumber, NSDate) : On vérifie que le nouvel objet n'est pas le même que l'ancien, on release l'ancien et on COPIE le nouveau
Pour une to-one relationship : idem, mais en RETIENT le nouveau
Pour une to-many : on initialise un NSMutableArray ou un NSMutableSet obligatoirement dans la méthode init .. mais comment déclarer la propriété ? Dans la doc, il est indiqué qu'il faut implémenter la méthode countOf<Key>, objectIn<Key>AtIndex:, ... Mais @synthetize fait-il cela ? faut-il donc se taper les accesseurs à la main dans le cas des to-many relationships
Oui.
Non, on le copie pas (envoi d'un -[copy]), on le retient ( -[retain]). Ces objets n'étant pas modifiables, à quoi bon créer une nouvelle instance en mémoire et copier ses attributs ?
Ceci dit, en pratique, la méthode -copy de ces objets fait simplement un -[retain], donc leur envoyer -[retain] ou -[copy] revient au même.
Du coup, la règle n'est pas différente de ci-dessus.
Tu confonds deux choses: l'accès à la collection et l'accès à son contenu.
Par exemple, pour un NSMutableArray, si on déclare la propriété
[tt]@property (retain) NSMutableArray *employees;[/tt]
C'est l'accès à la collection qu'on déclare.
Comme tu le dis, en général, on l'alloue dans -init et on ne veut pas qu'un autre objet y touche, donc on va plutôt le déclarer ainsi:
[tt]@property (readonly) NSMutableArray *employees;[/tt]
Ainsi, seul le getter sera généré.
Mais cette approche n'est pas très bonne, parce que les autres objets peuvent mettre le souk dans notre NSMutableArray: ajouter, supprimer des objets, les réordonner. On peut préférer ne pas exposer le NSMutableArray du tout, et obliger les autres objets à toujours passer par notre classe.
Dans ce cas, tu coderas les méthodes countOf<Key> et cie. Note qu'il ne s'agit pas de propriétés. Respecter les noms est important pour le Key-Value Coding (voir le guide).
NSLock, @synchronized, ou pas, c'est une habitude que j'ai pris et je ne vois pas en quoi elle serait mauvaise en tout cas.. Effectivement ça a son utilité dans une appli multi-threadée, mais je ne vois pas vraiment en quoi ça poserait problème de l'utiliser même dans un cas non multi-thread.
Quand utilise-t-on donc la copy/mutableCopy ? J'avais lu quelque part dans la doc qu'il fallait copier lorsqu'il s'agit d'attributs, et retenir quand il s'agit de relations...
Non, même dans une application monothreadée ca a son intérêt... Petit exemple:
Dans l'absolu, oui. Ce qu'on recherche dans la POO ce que chaque objet soit le plus indépendant possible des autres.
Ceci dit, c'est quand même fastidieux et perso, à moins d'utiliser le Key-Value Observing, je déclare la collection en propriété la plupart du temps.
Il ne peut pas y avoir de règle stricte; ça dépend de l'architecture.
En gros, quand on nous passe un objet mutable, si on veut être sûr qu'il ne va pas bouger il faut le copier.
Pour les relations, pareil. Si on veut être sûr que rien ne bougera, il faut tout copier. Note que si tu copies un NSMutableArray, l'instance sera bien différente, mais pas contre les objets qu'il contient seront les mêmes. Il faudrait aussi copier tous les objets (en suivant leurs relations) pour s'assurer que rien ne bouge !
C'est histoires de copie n'ont rien de simple. Essaie un jour de jouer avec l'Undo manager, tu vas voir, c'est plein de bonheur.
tu es serieux la?
ou juste pour donner un exemple ;-)
Je suis toujours sérieux...
Si dans le getter il n'y a pas de "retain autorelease", alors la methode dealloc appelée lors du release de l'objet foo aura pour effet la destruction de l'objet pointé par sa variable d'instance clef. Et par conséquent fooKey pointera sur un objet détruit...
... Et appeler la méthode bar sur un objet détruit aura très certainement un effet explosif
Par contre en inversant les 2 dernières lignes de mon exemple il n'y aura pas de plantage. Mais c'est prendre un risque de contraindre le développeur à faire attention au sens dans lequel il libère les objets... Et c'est même parfois compliqué dans du "vrai" code.
voila +propre et +lisible....
Et puis ta solution va à l'encontre des préconisations d'apple: C'est l'objet FooObject qui est responsable de la gestion mémoire des objets retournés par ses getters (il en est propriétaire puisque leur nom ne commence ni par init, ni par copy), l'utilisateur de ses méthodes ne doit pas effectuer d'appels à retain/release lorsqu'il ne désire pas conserver l'objet au delà de l'itération courante de la boucle de messages.
Maintenant, tant que c'est dans du code privé, c'est ton problème. Mais le jour où tu publieras un framework, les utilisateurs de ton framework n'accepteront certainement la contrainte que tu leur impose, puisque les autres frameworks n'ont pas cette contrainte.
ce code est t'il valide?
NSArray *array = ....
id obj = [array objectAtIndex:5];
[array release];
[obj bar];
Non et Oui. Enfin ça dépend de bien d'autres choses.. Déjà d'où vient 'obj'? Est-ce qu'il a été instancié au moment où tu as instancié la array? Dans ce cas, le code reste valide.
Quand bien même, je trouve ça assez moche comme façon d'écrire. Pour moi si tu accèdes à un objet d'une array, mais que tu l'utilises après avoir release cette array, berk.
Le code le plus lisible reste de toujours de release en dernier les objets que l'on instancie en premier.
et que fait zoc dans son exemple?
il utilise une variable d'instance après avoir détruit l'objet.
je suis entierement d'accord c'est beurk, d'ou l'écriture explicite et non implicite.
Pour moi non car tu n'as pas fait alloc/retain/copy sur array donc tu n'as pas à faire release, ou alors je n'ai pas bien compris les règles régissant la mémoire dans Cocoa, ce qui n'est pas impossible..
objectAtIndex: fait il un retain/autorelease?
Zoc le laissait sous entendre
Un moyen simple de tester:
;
Si ça ne plante pas, c'est donc que objectAtIndex: fait bien un retain/autorelease. Et ça confirme donc une certaine sécurité pour ceux qui codent mal (dans le cas d'une appli non multi-thread encore une fois.)
Edit: et effectivement, j'ai bien un résultat. Donc objectAtIndex: fait un retain/autorelease.
Tout laisse penser que Cocoa affecte une constant @foo et qu'elle reste disponible...
Quelque soit la nouvelle string faite avec @foo elle a la même adresse (0x100001060) et un retainCount de -1 qui doit être la manière dont Cocoa gère les constantes...
Rien à faire, je ne me fierais pas à ça ...
lauDupont | lauDupond
Je vais répondre moi même a la question puisque je viens de tester... NON
Faire le test avec un objet NSString n'est pas significatif car les constantes chaines sont réutilisées tant que possible et par conséquent jamais détruites.
J'ai fait mon test avec un NSNumber initialisé avec un float aléatoire (histoire de ne pas obtenir un objet provenant d'un cache quelconque), et effectivement le retain count reste inchangé lors d'un appel à objectAtIndex.
J'avoue que je suis assez surpris, mais bon, mea culpa, je me suis trompé (même si je persiste à penser que la version avec retain/autorelease est plus sûre et ne coûte pas grand chose).
Edit: En fait non, je ne me suis pas trompé, les 2 implémentations sont valables et décrites dans la documentation officielle.
Maintenant, l'exemple de code donné est mauvais parce qu'une NSString n'est pas modifiable. S'il passaient une NSMutableString, oui, il serait valable.
Ce que je veux dire, c'est que les ingés d'Apple sont souvent plus balèzes que nous autres, mais eux aussi ça les ennuie d'écrire des pages de doc plutôt que coder des trucs intéressants. Alors, ils font au plus simple, et parfois leurs exemples illustrent mal leur propos.
Donc si on résume les choses :
un accesseur "robuste" qui évite les crashs dans un code du genre :
Si on veut éviter cela, il faut donc un getter de la forme
et le setter sera de la forme :
Par contre, il y a perte de performance en cas d'accès fréquents au getter.
Est-ce vos pratiques habituelles ? ou bien évitez-vous le premier code par exemple en retenant vous-même lAncienneValeur ou en la copiant pour l'utiliser et ainsi utiliser les accesseurs "classiques" :
Ce dernier sait pertinemment qu'il va redéfinir la valeur d'obj et devient donc le nouveau propriétaire de l'ancienne valeur qu'il souhaite conserver.
Personnellement j'utilise souvent cette logique qui correspond à l'implémentation des accesseurs générés par le compilo suite à l'utilisation de :
Cependant, dans le cas d'une application multi-thread et d'une variable identifiée comme "à accès concurrents", il semble en effet beaucoup plus logique d'utiliser :
qui va automatiquement effectuer un [[valeur retain] autorelease] dans le getter.
+1 pour les @property, par contre je ne savais pas que le getter généré par un @property(retain, nonatomic) faisait aussi un retain+autorelease...