Pourquoi utiliseriez-vous un ivar?

153

Je vois généralement cette question posée dans l'autre sens, par exemple: Chaque ivar doit-il être une propriété? (et j'aime la réponse de bbum à ce Q).

J'utilise des propriétés presque exclusivement dans mon code. De temps en temps, cependant, je travaille avec un entrepreneur qui développe depuis longtemps sur iOS et qui est un programmeur de jeux traditionnel. Il écrit du code qui ne déclare presque aucune propriété et s'appuie sur ivars. Je suppose qu'il le fait parce que 1.) il y est habitué puisque les propriétés n'existaient pas toujours avant Objective C 2.0 (oct '07) et 2.) pour le gain de performance minimal de ne pas passer par un getter / setter.

Bien qu'il écrit du code qui ne fuit pas, je préférerais toujours qu'il utilise des propriétés plutôt que des ivars. Nous en avons parlé et il ne voit plus ou moins aucune raison d'utiliser des propriétés puisque nous n'utilisions pas KVO et qu'il a l'habitude de s'occuper des problèmes de mémoire.

Ma question est plus ... Pourquoi voudriez-vous jamais utiliser une période ivar - expérimentée ou non. Y a-t-il vraiment une telle différence de performances que l'utilisation d'un ivar serait justifiée?

Aussi comme point de clarification, je remplace les setters et les getters selon les besoins et j'utilise l'ivar qui correspond à cette propriété à l'intérieur du getter / setter. Cependant, en dehors d'un getter / setter ou d'un init, j'utilise toujours la self.myPropertysyntaxe.


Modifier 1

J'apprécie toutes les bonnes réponses. Une chose que j'aimerais aborder et qui semble incorrecte est qu'avec un ivar, vous obtenez l'encapsulation là où avec une propriété vous ne le faites pas. Définissez simplement la propriété dans une continuation de classe. Cela cachera la propriété aux étrangers. Vous pouvez également déclarer la propriété en lecture seule dans l'interface et la redéfinir comme readwrite dans l'implémentation comme:

// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;

et avoir dans la suite de la classe:

// readwrite within this file
@property (nonatomic, copy) NSString * name;

Pour l'avoir complètement "privé", ne le déclarez que dans la suite de la classe.

Sam
la source
2
vote positif pour une question intéressante - bien posée et aussi une que j'aimerais entendre le cas des ivars car il semble que l'on m'ait appris à le faire à la manière de Sam.
Damo
2
Notez que le comptage automatique de références (ARC) applique les mêmes avantages de gestion de la mémoire aux ivars que les propriétés, donc dans le code ARC, la différence concerne réellement l'encapsulation.
benzado
1
Votre question et en particulier la partie Edit 1 en fait beaucoup plus informative que la réponse choisie.
user523234
1
Pour Edit1: Je pense qu'il est possible de lire ET d'écrire chaque propriété, même si une seule déclaration de lecture seule en .h, avec Key-Value-Coding, par exemple: [object setValue: [NSNumber numberWithInt: 20] forKey: @ "propertyname "];
Binarian
1
@Sam à votre Edit 1: Si vous utilisez une propriété privée et utilisez l'extension / continuation de classe dans le fichier .m, elle n'est pas visible pour les sous-classes. Vous devez réécrire le code ou utiliser un autre .h avec l'extension de classe. Plus facile avec @ protected / default.
Binarian

Réponses:

100

Encapsulation

Si l'ivar est privé, les autres parties du programme ne peuvent pas y accéder aussi facilement. Avec une propriété déclarée, les personnes intelligentes peuvent accéder et muter assez facilement via les accesseurs.

Performance

Oui, cela peut faire une différence dans certains cas. Certains programmes ont des contraintes où ils ne peuvent pas utiliser de messagerie objc dans certaines parties du programme (pensez en temps réel). Dans d'autres cas, vous voudrez peut-être y accéder directement pour plus de vitesse. Dans d'autres cas, c'est parce que la messagerie objc agit comme un pare-feu d'optimisation. Enfin, il peut réduire vos opérations de comptage de références et minimiser l'utilisation maximale de la mémoire (si cela est fait correctement).

Types non triviaux

Exemple: si vous avez un type C ++, l'accès direct est parfois la meilleure approche. Le type peut ne pas être copiable ou ne pas être simple à copier.

Multithreading

Beaucoup de vos ivars sont codépendants. Vous devez garantir l'intégrité de vos données dans un contexte multithread. Ainsi, vous pouvez favoriser l'accès direct à plusieurs membres dans les sections critiques. Si vous vous en tenez aux accesseurs pour les données codépendantes, vos verrous doivent généralement être réentrants et vous finirez souvent par faire beaucoup plus d'acquisitions (beaucoup plus parfois).

Exactitude du programme

Puisque les sous-classes peuvent remplacer n'importe quelle méthode, vous pouvez éventuellement voir qu'il existe une différence sémantique entre l'écriture dans l'interface et la gestion appropriée de votre état. L'accès direct pour l'exactitude du programme est particulièrement courant dans les états partiellement construits - dans vos initialiseurs et dans dealloc, il est préférable d'utiliser l'accès direct. Vous pouvez également trouver ce courant dans les mises en œuvre d'un accesseur, un constructeur de commodité, copy,mutableCopy et mises en œuvre archivage / sérialisation.

Il est également plus fréquent que l'on passe du tout a un état d'esprit d' accesseur de lecture-écriture public à un état d'esprit qui cache bien ses détails / données d'implémentation. Parfois, vous devez contourner correctement les effets secondaires qu'un remplacement d'une sous-classe peut introduire afin de faire la bonne chose.

Taille binaire

Déclarer tout readwrite par défaut entraîne généralement de nombreuses méthodes d'accès dont vous n'avez jamais besoin, lorsque vous considérez l'exécution de votre programme pendant un moment. Cela ajoutera donc de la graisse à votre programme et aux temps de chargement.

Minimise la complexité

Dans certains cas, il est tout simplement inutile d'ajouter + type + maintenir tout cet échafaudage supplémentaire pour une variable simple telle qu'un booléen privé qui est écrit dans une méthode et lu dans une autre.


Cela ne veut pas du tout dire que l'utilisation de propriétés ou d'accesseurs est mauvaise - chacune présente des avantages et des restrictions importants. Comme de nombreux langages OO et approches de conception, vous devez également favoriser les accesseurs avec une visibilité appropriée dans ObjC. Il y aura des moments où vous devrez dévier. Pour cette raison, je pense qu'il est souvent préférable de restreindre les accès directs à l'implémentation qui déclare l'ivar (par exemple le déclarer @private).


re Edit 1:

La plupart d'entre nous ont mémorisé comment appeler dynamiquement un accesseur caché (tant que nous connaissons le nom…). Pendant ce temps, la plupart d'entre nous n'ont pas mémorisé comment accéder correctement aux ivars qui ne sont pas visibles (au-delà de KVC). La continuation de classe aide , mais elle introduit des vulnérabilités.

Cette solution de contournement est évidente:

if ([obj respondsToSelector:(@selector(setName:)])
  [(id)obj setName:@"Al Paca"];

Maintenant, essayez-le avec un ivar uniquement, et sans KVC.

Justin
la source
@Sam merci, et bonne question! re complexité: il va certainement dans les deux sens. re encapsulation - mis à jour
justin
@bbum RE: Exemple spécieux Bien que je sois d'accord avec vous pour dire que ce n'est pas la bonne solution, je ne peux pas imaginer que beaucoup de développeurs objc expérimentés pensent que cela n'arrive tout simplement pas; Je l'ai vu dans d'autres programmes et les App Stores sont allés jusqu'à interdire l'utilisation des API Apple privées.
justin
1
Vous ne pouvez pas accéder à un ivar privé avec object-> foo? Pas si difficile à retenir.
Nick Lockwood
1
Je voulais dire que vous pouvez y accéder en utilisant une déférence de pointeur de l'objet en utilisant la syntaxe C ->. Les classes Objective-C ne sont fondamentalement que des structures sous le capot, et étant donné un pointeur vers une structure, la syntaxe C pour accéder aux membres est ->, qui fonctionne aussi pour les ivars dans les classes objectives C.
Nick Lockwood
1
@NickLockwood si l'ivar l'est @private, le compilateur devrait interdire l'accès aux membres en dehors des méthodes de classe et d'instance - n'est-ce pas ce que vous voyez?
justin
76

Pour moi, c'est généralement la performance. Accéder à un ivar d'un objet est aussi rapide que d'accéder à un membre struct en C en utilisant un pointeur vers la mémoire contenant un tel struct. En fait, les objets Objective-C sont essentiellement des structures C situées dans une mémoire allouée dynamiquement. C'est généralement aussi rapide que votre code peut obtenir, même le code d'assemblage optimisé à la main ne peut pas être plus rapide que cela.

L'accès à un ivar via un getter / paramètre implique un appel de méthode Objective-C, qui est beaucoup plus lent (au moins 3-4 fois) qu'un appel de fonction C "normal" et même un appel de fonction C normal serait déjà plusieurs fois plus lent que accéder à un membre de structure. Selon les attributs de votre propriété, l'implémentation setter / getter générée par le compilateur peut impliquer un autre appel de fonction C aux fonctions objc_getProperty/ objc_setProperty, car celles-ci devront retain/ copy/ autoreleaseles objets selon les besoins et effectuer en outre le verrouillage de rotation pour les propriétés atomiques si nécessaire. Cela peut facilement devenir très coûteux et je ne parle pas d'être 50% plus lent.

Essayons ça:

CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    [self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

Production:

1: 23.0 picoseconds/run
2: 98.4 picoseconds/run

C'est 4,28 fois plus lent et c'était un int primitif non atomique, à peu près le meilleur des cas ; la plupart des autres cas sont encore pires (essayez unNSString * propriété !). Donc, si vous pouvez vivre avec le fait que chaque accès ivar est 4 à 5 fois plus lent qu'il ne pourrait l'être, utiliser les propriétés est bien (au moins en ce qui concerne les performances), cependant, il existe de nombreuses situations dans lesquelles une telle baisse de performance est complètement inacceptable.

Mise à jour 20/10/2015

Certaines personnes affirment que ce n'est pas un problème du monde réel, le code ci-dessus est purement synthétique et vous ne le remarquerez jamais dans une application réelle. Bon alors, essayons un échantillon du monde réel.

Le code ci-dessous définit les Accountobjets. Un compte a des propriétés qui décrivent le nom ( NSString *), le sexe ( enum) et l'âge ( unsigned) de son propriétaire, ainsi qu'un solde ( int64_t). Un objet de compte a une initméthode et uncompare: méthode. lecompare: méthode est définie comme suit: Ordres féminins avant les hommes, ordre alphabétique des noms, ordres jeunes avant vieux, ordres d'équilibre de bas en haut.

En fait, il existe deux classes de comptes, AccountAet AccountB. Si vous regardez leur implémentation, vous remarquerez qu'elles sont presque entièrement identiques, à une exception près: la compare:méthode. AccountAles objets accèdent à leurs propres propriétés par méthode (getter), tandis que les AccountBobjets accèdent à leurs propres propriétés par ivar. C'est vraiment la seule différence! Ils accèdent tous les deux aux propriétés de l'autre objet à comparer par getter (y accéder par ivar ne serait pas sûr! Et si l'autre objet est une sous-classe et a remplacé le getter?). Notez également que l'accès à vos propres propriétés en tant qu'ivars ne rompt pas l'encapsulation (les ivars ne sont toujours pas publics).

La configuration du test est vraiment simple: créez 1 Mio de comptes aléatoires, ajoutez-les à un tableau et triez ce tableau. C'est tout. Bien sûr, il existe deux tableaux, un pour les AccountAobjets et un pour les AccountBobjets et les deux tableaux sont remplis de comptes identiques (même source de données). Nous chronométrons le temps nécessaire pour trier les tableaux.

Voici la sortie de plusieurs courses que j'ai faites hier:

runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039
runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076

Comme vous pouvez le voir, le tri du tableau d' AccountBobjets est toujours plus rapide que le tri du tableau d' AccountAobjets.

Quiconque prétend que des différences d'exécution allant jusqu'à 1,32 seconde ne font aucune différence devrait mieux ne jamais faire de programmation d'interface utilisateur. Si je veux changer l'ordre de tri d'une grande table, par exemple, des différences de temps comme celles-ci font une énorme différence pour l'utilisateur (la différence entre une interface utilisateur acceptable et une interface utilisateur lente).

Dans ce cas également, l'exemple de code est le seul vrai travail effectué ici, mais à quelle fréquence votre code est-il juste un petit engrenage d'une horloge compliquée? Et si chaque engrenage ralentit tout le processus comme celui-ci, qu'est-ce que cela signifie pour la vitesse de l'ensemble du mouvement d'horlogerie à la fin? Surtout si une étape de travail dépend de la sortie d'une autre, ce qui signifie que toutes les inefficacités se résumeront. La plupart des inefficacités ne sont pas un problème en soi, c'est leur somme qui devient un problème pour l'ensemble du processus. Et un tel problème n'est rien qu'un profileur montrera facilement, car un profileur consiste à trouver des points chauds critiques, mais aucune de ces inefficacités n'est en soi des points chauds. Le temps processeur n'est que moyennement réparti entre eux, mais chacun d'entre eux n'en a qu'une infime fraction, il semble que l'optimiser soit une perte de temps totale. Et c'est vrai,

Et même si vous ne pensez pas en termes de temps CPU, parce que vous pensez que gaspiller du temps CPU est tout à fait acceptable, après tout «c'est gratuit», qu'en est-il des coûts d'hébergement de serveur causés par la consommation d'énergie? Qu'en est-il de l'autonomie de la batterie des appareils mobiles? Si vous écrivez la même application mobile deux fois (par exemple un propre navigateur Web mobile), une fois une version où toutes les classes accèdent à leurs propres propriétés uniquement par des getters et une fois où toutes les classes y accèdent uniquement par ivars, utiliser le premier constamment épuisera définitivement la batterie beaucoup plus rapide que l'utilisation de la seconde, même si elles sont fonctionnelles équivalentes et pour l'utilisateur la seconde serait même probablement même un peu plus rapide.

Voici maintenant le code de votre main.mfichier (le code repose sur l'activation d'ARC et assurez-vous d'utiliser l'optimisation lors de la compilation pour voir l'effet complet):

#import <Foundation/Foundation.h>

typedef NS_ENUM(int, Gender) {
    GenderMale,
    GenderFemale
};


@interface AccountA : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountA *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


@interface AccountB : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountB *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


static
NSMutableArray * allAcocuntsA;

static
NSMutableArray * allAccountsB;

static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
    assert(min <= max);
    uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
    rnd = (rnd << 32) | arc4random();
    rnd = rnd % ((max + 1) - min); // Trim it to range
    return (rnd + min); // Lift it up to min value
}

static
void createAccounts ( const NSUInteger ammount ) {
    NSArray *const maleNames = @[
        @"Noah", @"Liam", @"Mason", @"Jacob", @"William",
        @"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
    ];
    NSArray *const femaleNames = @[
        @"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
        @"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
    ];
    const NSUInteger nameCount = maleNames.count;
    assert(maleNames.count == femaleNames.count); // Better be safe than sorry

    allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
    allAccountsB = [NSMutableArray arrayWithCapacity:ammount];

    for (uint64_t i = 0; i < ammount; i++) {
        const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
        const unsigned age = (unsigned)getRandom(18, 120);
        const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;

        NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
        const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
        NSString *const name = nameArray[nameIndex];

        AccountA *const accountA = [[AccountA alloc]
            initWithName:name age:age gender:g balance:balance
        ];
        AccountB *const accountB = [[AccountB alloc]
            initWithName:name age:age gender:g balance:balance
        ];

        [allAcocuntsA addObject:accountA];
        [allAccountsB addObject:accountB];
    }
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            NSUInteger ammount = 1000000; // 1 Million;
            if (argc > 1) {
                unsigned long long temp = 0;
                if (1 == sscanf(argv[1], "%llu", &temp)) {
                    // NSUIntegerMax may just be UINT32_MAX!
                    ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
                }
            }
            createAccounts(ammount);
        }

        // Sort A and take time
        const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;

        // Sort B and take time
        const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAccountsB sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;

        NSLog(@"runTime 1: %f", runTime1);
        NSLog(@"runTime 2: %f", runTime2);
    }
    return 0;
}



@implementation AccountA
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (self.gender != account.gender) {
            if (self.gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![self.name isEqualToString:account.name]) {
            return [self.name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (self.age != account.age) {
            if (self.age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (self.balance != account.balance) {
            if (self.balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end


@implementation AccountB
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (_gender != account.gender) {
            if (_gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![_name isEqualToString:account.name]) {
            return [_name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (_age != account.age) {
            if (_age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (_balance != account.balance) {
            if (_balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end
Mecki
la source
3
Explication extrêmement informative et terre-à-terre. Vote positif pour l'exemple de code
Philip007
1
L'un des qualificatifs clés que je vois dans votre message est "... à partir des chemins de code critiques". Le fait est d'utiliser ce qui rend le code plus facile à lire / écrire, puis d'optimiser ce que vous trouvez être les chemins critiques. Cela ajoutera de la complexité là où c'est nécessaire.
Sandy Chapman
1
@ViktorLexington Dans mon code, je définissais un unsigned intqui n'est jamais conservé / libéré, que vous utilisiez ARC ou non. La conservation / libération elle-même est coûteuse, donc la différence sera moindre car la gestion de la conservation ajoute une surcharge statique qui existe toujours, en utilisant directement setter / getter ou ivar; cependant, vous économiserez toujours la surcharge d'un appel de méthode supplémentaire si vous accédez directement à l'ivar. Ce n'est pas grave dans la plupart des cas, à moins que vous ne le fassiez plusieurs milliers de fois par seconde. Apple dit d'utiliser des getters / setters par défaut, sauf si vous êtes dans une méthode init / dealloc ou si vous avez repéré un goulot d'étranglement.
Mecki
1
@Fogmeister Ajout d'un exemple de code qui montre à quel point cela peut facilement faire une énorme différence dans un exemple très simple du monde réel. Et cet exemple n'a rien à voir avec un super ordinateur effectuant des milliards de calculs, il s'agit plutôt de trier une table de données très simple (un cas assez courant parmi des millions d'applications).
Mecki
2
@malhal Une propriété marquée comme copyne fera PAS une copie de sa valeur à chaque fois que vous y accédez. Le getter de copypropriété est comme le getter d'une propriété strong/ retain. Son code est fondamentalement return [[self->value retain] autorelease];. Seul le setter copie la valeur et cela ressemblera à peu près à ceci [self->value autorelease]; self->value = [newValue copy];, alors qu'un strong/ retainsetter ressemble à ceci:[self->value autorelease]; self->value = [newValue retain];
Mecki
9

La raison la plus importante est le concept OOP de dissimulation d'informations : si vous exposez tout via des propriétés et que vous permettez ainsi aux objets externes de jeter un coup d'œil sur les composants internes d'un autre objet, vous utiliserez ces éléments internes et compliquerez ainsi la modification de l'implémentation.

Le gain de «performances minimales» peut rapidement se résumer et devenir un problème. Je sais par expérience; Je travaille sur une application qui pousse vraiment les iDevices à leurs limites et nous devons donc éviter les appels de méthode inutiles (bien sûr uniquement lorsque cela est raisonnablement possible). Pour aider à atteindre cet objectif, nous évitons également la syntaxe de point car elle rend difficile la visualisation du nombre d'appels de méthode à première vue: par exemple, combien d'appels de méthode l'expression self.image.size.widthdéclenche-t-elle? En revanche, vous pouvez immédiatement dire avec [[self image] size].width.

De plus, avec une dénomination ivar correcte, KVO est possible sans propriétés (IIRC, je ne suis pas un expert du KVO).

DarkDust
la source
3
+1 Bonne réponse concernant le gain de "performances minimales" additionné et souhaitant voir tous les appels de méthode explicitement. L'utilisation de la syntaxe dot avec les propriétés masque définitivement beaucoup de travail qui se passe dans les getters / setters personnalisés (surtout si ce getter renvoie une copie de quelque chose à chaque fois qu'il est appelé).
Sam
1
KVO ne fonctionne pas pour moi sans utiliser un setter. Changer directement l'ivar n'appelle pas l'observateur que la valeur a changé!
Binarian
2
KVC peut accéder à ivars. KVO ne peut pas détecter les modifications apportées aux ivars (et s'appuie à la place sur les accesseurs à appeler).
Nikolai Ruhe
9

Sémantique

  • Que @propertypeut exprimer que les ivars ne peuvent pas: nonatomicet copy.
  • Ce que les ivars peuvent exprimer qui @propertyne le peuvent pas:
    • @protected: public sur sous-classes, privé à l'extérieur.
    • @package: public sur frameworks sur 64 bits, privé à l'extérieur. Identique @publicà 32 bits. Voir Contrôle d'accès aux variables de classe et d'instance 64 bits d' Apple .
    • Qualificateurs. Par exemple, les tableaux de solides références d'objet: id __strong *_objs.

Performance

Petite histoire: les ivars sont plus rapides, mais cela n'a pas d'importance pour la plupart des utilisations. nonatomicLes propriétés n'utilisent pas de verrous, mais direct ivar est plus rapide car il ignore l'appel des accesseurs. Pour plus de détails, lisez l' e-mail suivant sur lists.apple.com.

Subject: Re: when do you use properties vs. ivars?
From: John McCall <email@hidden>
Date: Sun, 17 Mar 2013 15:10:46 -0700

Les propriétés affectent les performances de plusieurs manières:

  1. Comme déjà discuté, l'envoi d'un message pour effectuer un chargement / stockage est plus lent que de simplement effectuer le chargement / stockage en ligne .

  2. Envoyer un message pour effectuer un chargement / stockage est également un peu plus de code qui doit être conservé dans l'i-cache: même si le getter / setter n'a ajouté aucune instruction supplémentaire au-delà du chargement / stockage, il y aurait une moitié solide - une douzaine d'instructions supplémentaires dans l'appelant pour configurer l'envoi du message et gérer le résultat.

  3. L'envoi d'un message force une entrée pour ce sélecteur à être conservée dans le cache de méthode , et cette mémoire reste généralement dans le d-cache. Cela augmente le temps de lancement, augmente l'utilisation de la mémoire statique de votre application et rend les changements de contexte plus douloureux. Étant donné que le cache de méthode est spécifique à la classe dynamique d'un objet, ce problème augmente au fur et à mesure que vous utilisez KVO.

  4. L'envoi d'un message force toutes les valeurs de la fonction à être déversées dans la pile (ou conservées dans des registres de sauvegarde de l'appelé, ce qui signifie simplement qu'elles se répandent à un moment différent).

  5. L'envoi d' un message peut avoir des effets secondaires arbitraires et donc

    • force le compilateur à réinitialiser toutes ses hypothèses sur la mémoire non locale
    • ne peut pas être hissé, coulé, réorganisé, fusionné ou éliminé.

  6. Dans ARC, le résultat d'un message envoyé sera toujours conservé , soit par l'appelé, soit par l'appelant, même pour les retours +0: même si la méthode ne retient pas / ne libère pas automatiquement son résultat, l'appelant ne le sait pas et a pour essayer de prendre des mesures pour éviter que le résultat ne soit automatiquement publié. Cela ne peut jamais être éliminé car les messages envoyés ne sont pas statiquement analysables.

  7. Dans ARC, comme une méthode setter prend généralement son argument à +0, il n'y a aucun moyen de "transférer" une rétention de cet objet (qui, comme discuté ci-dessus, ARC a généralement) dans l'ivar, donc la valeur doit généralement obtenir conserver / relâcher deux fois .

Rien de tout cela ne signifie qu'ils sont toujours mauvais, bien sûr - il y a beaucoup de bonnes raisons d'utiliser des propriétés. Gardez simplement à l'esprit que, comme beaucoup d'autres fonctionnalités linguistiques, elles ne sont pas gratuites.


John.

Jano
la source
6

Propriétés vs variables d'instance est un compromis, en fin de compte, le choix se résume à l'application.

Encapsulation / masquage des informations C'est une bonne chose (TM) du point de vue de la conception, des interfaces étroites et un lien minimal sont ce qui rend le logiciel maintenable et compréhensible. Il est assez difficile dans Obj-C de cacher quoi que ce soit, mais les variables d'instance déclarées dans l' implémentation sont aussi proches que possible.

Performances Alors que "l'optimisation prématurée" est une mauvaise chose (TM), écrire du code peu performant simplement parce que vous le pouvez est au moins aussi mauvais. Il est difficile d'argumenter contre un appel de méthode plus cher qu'une charge ou un stockage, et dans le code intensif en calcul, le coût s'additionne rapidement.

Dans un langage statique avec des propriétés, comme C #, les appels aux setters / getters peuvent souvent être optimisés par le compilateur. Cependant, Obj-C est dynamique et la suppression de tels appels est beaucoup plus difficile.

Abstraction Un argument contre les variables d'instance dans Obj-C a toujours été la gestion de la mémoire. Avec les variables d'instance MRC, les appels à retenir / release / autorelease doivent être répartis dans tout le code, les propriétés (synthétisées ou non) gardent le code MRC en un seul endroit - le principe de l'abstraction qui est une bonne chose (TM). Cependant, avec GC ou ARC, cet argument disparaît, donc l'abstraction pour la gestion de la mémoire n'est plus un argument contre les variables d'instance.

CRD
la source
5

Les propriétés exposent vos variables à d'autres classes. Si vous avez juste besoin d'une variable uniquement relative à la classe que vous créez, utilisez une variable d'instance. Voici un petit exemple: les classes XML pour l'analyse RSS et autres parcourent un tas de méthodes déléguées et autres. Il est pratique d'avoir une instance de NSMutableString pour stocker le résultat de chaque passe différente de l'analyse. Il n'y a aucune raison pour qu'une classe extérieure ait besoin d'accéder ou de manipuler cette chaîne. Donc, vous le déclarez simplement dans l'en-tête ou en privé et y accédez dans toute la classe. Définir une propriété pour cela peut être utile uniquement pour s'assurer qu'il n'y a pas de problèmes de mémoire, en utilisant self.mutableString pour appeler les getter / setters.

Justin
la source
5

La rétrocompatibilité a été un facteur pour moi. Je ne pouvais utiliser aucune fonctionnalité d'Objective-C 2.0 car je développais des logiciels et des pilotes d'imprimante qui devaient fonctionner sur Mac OS X 10.3 dans le cadre d'une exigence. Je sais que votre question semblait ciblée sur iOS, mais je pensais que je partagerais toujours mes raisons de ne pas utiliser de propriétés.

dreamlax
la source