héritage Objective C

samirsamir Membre
juillet 2014 modifié dans API UIKit #1

Hello,


 


J'ai un souci avec l'héritage. Voila ce que je voudrais faire :


 


static CGFloat defaultHeight = 0.0;


 


@interface SGBaseTableViewCell : UITableViewCell


 


+ (CGFloat)defaultHeight;


....


 


@end


 


 


@interface ;SGBaseTableViewCell : UITableViewCell


 


+ (CGFloat)defaultHeight {


 


 if (defaultHeight == 0.0) {


        SGBaseTableViewCell *cell = [[self class] newDefaultCell]; 


        defaultHeight = cell.height;


    }


    return defaultHeight;


}


 


@end


 

// newDefaultCell Méthode utilitaire qui me charge un xib

​// height méthode qui me renvoie la hauteur de la vue.

 

Donc voila, j'ai une classe mère SGBaseTableViewCell qui me produit des méthodes utilitaires pour les classes filles. Le problème avec ce code ce qu'il va me retourner à  chaque fois la même hauteur, alors que moi je veux que ça retourne la hauteur de la cellule de chaque classe fille. 

 

Le problème viens du fait que la variable globale defaultHeight appartient au scope de la classe mère et non pas aux classes fille.

 

Solution 1 : 

 

Il faut redéfinir toutes les méthodes de classe mère dans les classes fille pour renvoyer la hauteur de chaque cellule mais je trouve ça dommage de ne pas le pouvoir le faire une seule fois dans la classe parente. 

 

Solution 2 : 

Si vous avez des suggestion, ça sera super :). Merci

Réponses

  • CéroceCéroce Membre, Modérateur
    En gros, il faut pouvoir associer une hauteur de cellule à  une classe.

    Quelques solutions, pas garanties du tout:
    - utiliser un dictionnaire, dont les clefs sont des NSValues qui contiennent la valeur de class... je ne suis même pas sûr qu'on puisse faire cela.
    - utiliser les objets associés (http://nshipster.com/associated-objects/). En Objective-C, une classe est un objet, et on doit pouvoir lui associer un NSNumber qui est sa hauteur par défaut.

    Voilà  pour le premier jet.
  • AliGatorAliGator Membre, Modérateur
    Moi j'aurai utilisé la première solution, à  savoir un dictionnaire... mais en utilisant NSStringFromClass comme clé plutôt qu'utiliser NSValue.
  • CéroceCéroce Membre, Modérateur
    Ah oui, Ali, je n'y avais pas pensé, ça semble une bonne solution.
  • Et du coup le dico est une variable globale static ?


  • AliGatorAliGator Membre, Modérateur
    Ouais. Mega beurk mais ouais. Et du coup ça oblige à  être extra careful concernant les accès concurrentiels & co, un peu dangereux...

    Le mieux est sans doute une static locale donc l'accès pourra être contrôlé plus localement. Genre :
    +(CGFloat)defaultHeight
    {
    static dispatch_once_t onceToken;
    static NSDictionary* heights;
    dispatch_once(&onceToken, ^{ heights = [NSDictionary new]; });

    @synchronize(self)
    {
    NSString* key = NSStringFromClass(self);
    NSNumber* h = height[key];
    if (h) return [h floatValue];
    SGBaseTableViewCell *cell = [self newDefaultCell];
    height[key] = @(cell.height);
    return cell.height;
    }
    }
    Et encore je tape ça au feeling faudrait vérifier la bonne thread-safety.
  • Merci pour vos réponses.


     




    Ah oui, Ali, je n'y avais pas pensé, ça semble une bonne solution.




    Oui je trouve aussi que c'est une bonne solution. 


     


     




    Et du coup le dico est une variable globale static ?




    Oui je vais faire ça, un dictionnaire en static initialisé avec un dispatch_one ou autre. 


  • AliGatorAliGator Membre, Modérateur
    juillet 2014 modifié #8

    Le problème viens du fait que la variable globale defaultHeight appartient au scope de la classe mère et non pas aux classes fille.

    Je n'en suis pas si sûr. C'est toute la subtilité avec les extern, static & co.

    Tu définis ta variable comme étant "static", ça veut dire qu'elle n'est visible que dans le scope dans lequel elle est déclarée. Si tu l'avais déclarée dans ton SGBaseTableViewCell.m, elle n'aurait été visible que dans ton SGBaseTableViewCell.m

    Sauf que là , tu l'as déclarée dans ton SGBaseTableViewCell.h. Donc à  chaque fois tu fais un #import "SGBaseTableViewCell.h", cela redéclare une nouvelle variable globale "static CGFloat defaultHeight = 0.0" qui est indépendante des autres, et qui n'existe que dans le scope du fichier dans lequel elle est compilée, donc dans le scope du fichier dans lequel tu as fait le #import "SGBaseTableViewCell.h".
    • Tu l'aurais déclarée en non-static, ça aurait été une variable globale non limitée au scope du fichier compilé. Tu aurais même eu une erreur pour redéfinition multiple de la variable.
    • Tu l'aurais déclarée en extern, ça aurait compilé car le mot clé "extern" n'alloue pas de mémoire pour la variable, il dit juste au compilateur qu'elle doit exister quelque part. Et du coup tu l'aurais déclarée en non-extern dans ton .m, pour qu'elle n'existe qu'à  un seul endroit, et tu n'aurais eu alors qu'une seule variable globale defaultHeight accessible de partout.
    • Mais là  tu l'as déclarée en static dans ton .h, donc autrement dit déclaré dans tous les fichiers qui le #import (puisqu'un #import n'est pas bcp + qu'un bête copier/coller du code du .h avant compilation du .m)
    Ce qui veut dire qu'écrit comme tel, ton code pourrait bien marcher : chaque fois que tu vas avoir une sous-classe de SGBaseTableViewCell, tu vas faire un #import "SGBaseTableViewCell.h" ce qui va te redéclarer une nouvelle variable defaultHeight dont le scope sera restreint au fichier SGSubClassTableViewCell.m depuis lequel tu fais l'import. Donc une variable différente, qui vaudra 0.

    La seule chose que je ne saurais te dire, et pour laquelle il faudrait faire des tests pour être sûr, c'est est-ce que dans ce cas-là , quand tu appelles la méthode "-(CGFloat)defaultHeight" sur ta SGSubClassTableViewCell, méthode qui n'est pas surchargée et donc qui va utiliser l'implémentation de la classe mère SGBaseTableViewCell, est-ce que comme c'est l'implémentation venant de SGBaseTableViewCell ne va-t-il pas utiliser la variable defaultHeight connue localement dans SGBaseTableViewCell et donc celle de la classe mère, même si tu en as déclarée une du même nom dans la classe fille ?
    Et là  en toute logique j'ai envie de dire que oui (ce qui donc ne solutionne pas ton problème puisque tu utiliserais alors toujours la même variable de la classe mère même si tu la redéclares dans la classe fille).

    Ceci dit de toute façon c'est une très mauvaise idée en général de déclarer une variable "static" dans un ".h" car :
    • En général on ne se rend pas compte que cette variable est redéclarée autant de fois que tu #import (directement ou indirectement) le .h en question. Ce qui donne vite un nombre important de variables ayant le même nom (mais des scopes différents)
    • Du coup ça devient très vite très perturbant (et surtout très difficile de s'y retrouver)
    • Et au final tu risques d'avoir des comportements qui vont te sembler bizarres justement à  cause de ça, et pour comprendre pourquoi ça arrive et quelle variable du nom de defaultHeight il utilise parmi la 10aine qui ont été déclarées à  force de #import, bah bon courage ^^
  • Merci pour toutes ces réponses détaillées.


     


     




    Ouais. Mega beurk mais ouais. Et du coup ça oblige à  être extra careful concernant les accès concurrentiels & co, un peu dangereux...


     




     


    Je ne comprends pas  c'est quoi qui n'est pas propre ? C'est la faite de mettre le code de calcule de la taille des cellules dans la classe mère ? si c'est la cas, tu l'aurais fais comment ?


     


    Je l'ai fais la première fois en déclarant la classe mère comme une classe abstraite en obligeons les classes filles de réécrire les méthode de la classe parente, tout simplement en utilisant les exceptions dans les implémentations de la classe mère, mais j'ai remarqué que le code des classes filles se ressemble, j'ai pensé à  une façon de factoriser tout ça dans la classe mère d'ou ma question, mais j'avoue que je suis plus perdu maintenant avec l'entrée de la "thread safety"...


     


     





    +(CGFloat)defaultHeight
    {
    static dispatch_once_t onceToken;
    static NSDictionary* heights;
    dispatch_once(&onceToken, ^{ heights = [NSDictionary new]; });

    @synchronize(self)
    {
    NSString* key = NSStringFromClass(self);
    NSNumber* h = height[key];
    if (h) return [h floatValue];
    SGBaseTableViewCell *cell = [self newDefaultCell];
    height[key] = @(cell.height);
    return cell.height;
    }
    }



     


    Pourquoi protèges-tu ce bout de code avec un lock ? je dirais que le faite y a une variable globale static, mais je ne suis pas sur. Si c'est oui tu mets à  chaque fois un lock à  la lecture/écriture d'une variable statique ? 


     


     





    La seule chose que je ne saurais te dire, et pour laquelle il faudrait faire des tests pour être sûr, c'est est-ce que dans ce cas-là , quand tu appelles la méthode "-(CGFloat)defaultHeight" sur ta SGSubClassTableViewCell, méthode qui n'est pas surchargée et donc qui va utiliser l'implémentation de la classe mère SGBaseTableViewCell, est-ce que comme c'est l'implémentation venant de SGBaseTableViewCell ne va-t-il pas utiliser la variable defaultHeight connue localement dans SGBaseTableViewCell et donc celle de la classe mère, même si tu en as déclarée une du même nom dans la classe fille ?

    Et là  en toute logique j'ai envie de dire que oui (ce qui donc ne solutionne pas ton problème puisque tu utiliserais alors toujours la même variable de la classe mère même si tu la redéclares dans la classe fille).




     


    et oui t'as bien raison :).

  • samirsamir Membre
    juillet 2014 modifié #10


     


    Et encore je tape ça au feeling faudrait vérifier la bonne thread-safety.


    y a un moyen de vérifier la Thread safety d'un programme ? , à  part de bien tester 


     


    PS : Désolé je voulais éditer le précédent post, mais ...


  • CéroceCéroce Membre, Modérateur

    Je ne comprends pas  c'est quoi qui n'est pas propre ? C'est la faite de mettre le code de calcule de la taille des cellules dans la classe mère ?

    Non, c'est le fait d'utiliser une variable globale. Selon que tu la déclares en static ou non, une nouvelle variable sera allouée pour chaque classe fille, ou pas. Il s'agit d'un comportement qui n'apparait pas clairement dans le code; voir la discussion plus haut.

    Ta solution ne paraà®t pas mauvaise, seulement ObjC oblige à  quelques contorsions avec les variables globales.
  • AliGatorAliGator Membre, Modérateur

    Je ne comprends pas c'est quoi qui n'est pas propre ? C'est la faite de mettre le code de calcule de la taille des cellules dans la classe mère ? si c'est la cas, tu l'aurais fais comment ?

    Oui comme l'a dit Ceroce, c'est le côté variable globale qui est beurk.
    Car les variables globales, c'est le mal, essentiellement car c'est l'enfer à  protéger contre des accès concurrents en environnement multi-threadé.

    Tu n'as aucune garantie que 2 bouts de code n'accèderont pas en même temps à  la variable globale, tous les 2 pour écrire dedans en même temps par exemple, chacun depuis son thread. Ce qui peut corrompre le dictionnaire en question ou bien donner des faux positifs (genre tu regardes si la clé existe, elle n'existe pas, et là  hop un autre thread prend la main et écrit ladite clé, qui du coup existe... et ton thread d'origine reprend la main dans le code qui ne devrait s'exécuter que si la clé n'existe pas alors que maintenant elle existe...).

    Bref, Thread-Safety-Hell. Alors qu'avec une variable d'instance et une @property tu peux la rendre atomique et protéger ses accès avec les setter/getter.

    C'est d'ailleurs pour ça que j'ai commencé à  insérer des protections avec les @synchronize, pour limiter les accès au dictionnaire que par un seul thread à  la fois pour éviter les problèmes.
    Et c'est aussi pour cela que j'ai mis la déclaration du dictionnaire static dans la méthode et non dans le scope global du fichier, pour avoir l'assurance que seule cette méthode ait accès au dictionnaire, que le dico ne soit accédé que depuis cet endroit et ne risque pas d'être accédé par une autre méthode, ça réduit les points de rupture potentiels au minimum, plutôt que d'avoir à  protéger 36 endroits où le dico serait accédé de partout ; comme ça on n'a qu'un seul endroit à  protéger contre les accès concurrents potentiels. Et du coup mon @synchronize a pour but que seul un thread à  la fois accède à  cette variable globale.

    Et ce qui pose surtout problème avec les variables globales c'est que les accès concurrents qui pourraient poser problème, ils arrivent genre une fois sur 10 donc tu ne les vois pas systématiquement, et ils sont donc difficiles à  reproduire à  la demande. Donc pour débuguer ça, c'est aussi assez casse-gueule.

    y a un moyen de vérifier la Thread safety d'un programme ? , à  part de bien tester

    Justement non pas vraiment, à  part connaà®tre les astuces et comprendre le fonctionnement assez en détail des threads et des risques classiques pour les parer au mieux, et de la réflexion pour n'oublier aucun use case, etc...

    Si tu n'as qu'un truc à  retenir, c'est que les variables globales à  tout le scope du fichier, c'est le mal. Ca peut marcher, mais faut être extrêmement prudent et c'est marcher sur des braises à  pieds nus. Faut se protéger de partout, penser à  tous les cas, tu peux vite en oublier et si c'est le cas et que tu termines avec un EXC_BADACCESS une fois sur 13, bah bien souvent c'est la galère pour comprendre pourquoi.
    Les constantes globales il n'y a pas de soucis, puisque c'est en lecture seule, mais les variables globales faut les protéger de partout... si on peut éviter ce pattern c'est mieux.

    Les static à  l'intérieur d'une méthode, ça reste en pratique une fois compilé une variable globale (commune à  toutes les classes et instances, etc) sauf que ce n'est connu qu'à  l'intérieur de la méthode donc tu ne peux y accéder que depuis l'intérieur de la méthode, ce qui limite les endroits où protéger tout ça.
  • Les NSMapTable sont plus adapté à  tout ce qui est indexation par objet (et donc par classe).


  • FKDEVFKDEV Membre
    juillet 2014 modifié #14
    Ne pourrais-tu pas déplacer le lazy-loading au niveau de la cellule (puisque c'est ce que tu fais déjà  mais de manière un peu indirecte).
    Dans chaque classe cellule, tu aurais le code suivant à  dupliquer.
      
    static DerivedCell* cell;
    + (instancetype) defaultCell
    {
      //TODO: use dispatch_once, if afraid of multithread
        if (!cell)
            cell = [[self class] newDefautCell];
        return cell;
    }
     
    Ca fait un peu de duplication de code, mais c'est acceptable quand la duplication est bête et systématique.


    Dans ta classe de base :
    + (CGFloat)defaultHeight {
    return [[[self class] defaultCell] preferredSize...]; 
    }

    + (CGFloat)preferredWidthForSizeClass:(...)sizeClass
    {
    return [[[self class] defaultCell] defaultHeight]; 
    }

     
     
    Bien-sûr cela consomme un peu plus de mémoire, mais c'est un peu plus évolutif car tu vas peut-etre te rendre compte que tu as besoin d'autres infos que la hauteur de la cellule.
Connectez-vous ou Inscrivez-vous pour répondre.