objective c - Perché dovresti usare un ivar?




objective-c ios (5)

Di solito vedo questa domanda posta dall'altra parte, come Must deve essere una proprietà di ivar? (e mi piace la risposta di bbum a questa Q).

Uso le proprietà quasi esclusivamente nel mio codice. Ogni tanto, tuttavia, lavoro con un appaltatore che è stato sviluppato su iOS per molto tempo ed è un programmatore di giochi tradizionale. Scrive codice che dichiara quasi nessuna proprietà e si appoggia su ivars. Presumo che lo faccia perché 1.) è abituato perché le proprietà non sempre esistono fino all'Obiettivo C 2.0 (Oct '07) e 2.) per il guadagno minimo di prestazioni di non passare attraverso un getter / setter.

Mentre scrive codice che non perde, preferirei comunque che usasse le proprietà su ivars. Ne abbiamo parlato e lui più o meno non vede ragioni per usare le proprietà dato che non stavamo usando KVO e ha esperienza nel prendersi cura dei problemi di memoria.

La mia domanda è più ... Perché mai vorresti usare un periodo di ivar - esperto o meno. C'è davvero una grande differenza di prestazioni che usare un ivar sarebbe giustificato?

Inoltre, come punto di chiarimento, sovrascrivo setter e getter a seconda delle necessità e uso l'ivar che si correla con quella proprietà all'interno del getter / setter. Tuttavia, al di fuori di un getter / setter o init, uso sempre la sintassi self.myProperty .

Modifica 1

Apprezzo tutte le buone risposte. Uno che mi piacerebbe indirizzare che sembra errato è che con un ivar si ottiene incapsulamento dove non si ha una proprietà. Basta definire la proprietà in una continuazione di classe. Questo nasconderà la proprietà agli estranei. Puoi anche dichiarare la proprietà di sola lettura nell'interfaccia e ridefinirla come readwrite nell'implementazione come:

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

e avere nella continuazione della classe:

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

Per averla completamente "privata", dichiarala solo nella continuazione della classe.


incapsulamento

Se ivar è privato, le altre parti del programma non possono arrivarci facilmente. Con una proprietà dichiarata, le persone intelligenti possono accedere e mutare abbastanza facilmente tramite gli accessor.

Prestazione

Sì, questo può fare la differenza in alcuni casi. Alcuni programmi hanno vincoli in cui non possono utilizzare alcun messaggio objc in alcune parti del programma (pensa in tempo reale). In altri casi, potresti voler accedere direttamente alla velocità. In altri casi, è perché la messaggistica objc funge da firewall di ottimizzazione. Infine, può ridurre le operazioni di conteggio dei riferimenti e minimizzare l'utilizzo di memoria di picco (se eseguito correttamente).

Tipi non banali

Esempio: se si dispone di un tipo C ++, l'accesso diretto è a volte l'approccio migliore. Il tipo potrebbe non essere copiabile o potrebbe non essere banale da copiare.

multithreading

Molti dei tuoi ivar sono codipendenti. È necessario garantire l'integrità dei dati nel contesto con multithreading. Pertanto, è possibile favorire l'accesso diretto a più membri nelle sezioni critiche. Se si attaccano con gli accessor per dati codipendenti, i blocchi devono tipicamente essere rientranti e spesso si finiscono per fare molte più acquisizioni (molto più a volte).

Programmare la correttezza

Dal momento che le sottoclassi possono sovrascrivere qualsiasi metodo, è possibile che alla fine ci sia una differenza semantica tra la scrittura sull'interfaccia e la gestione appropriata del proprio stato. L'accesso diretto per la correttezza del programma è particolarmente comune negli stati parzialmente costruiti: nei tuoi inizializzatori e in dealloc , è preferibile utilizzare l'accesso diretto. Si può anche trovare questo comune nelle implementazioni di un accessorio, un costruttore di convenienza, copy , mutableCopy e implementazioni di archiviazione / serializzazione.

È anche più frequente quando si passa da tutto ciò che ha una mentalità di accesso pubblico di readwrite a uno che nasconde bene i dettagli / i dati di implementazione. A volte è necessario superare correttamente gli effetti collaterali che una sottoclasse di override può introdurre per fare la cosa giusta.

Dimensione binaria

Dichiarare tutto ciò che readwrite per impostazione predefinita di solito comporta molti metodi di accesso che non è necessario, quando si considera l'esecuzione del programma per un momento. Così aggiungerà un po 'di grasso al tuo programma e caricherà anche i tempi.

Minimizza la complessità

In alcuni casi, è completamente inutile aggiungere + type + mantenere tutto lo scaffolding extra per una variabile semplice come un bool privato che viene scritto in un metodo e letto in un altro.

Questo non significa affatto che usare proprietà o accessorie sia sbagliato - ognuno ha importanti benefici e restrizioni. Come molte altre lingue OO e approcci alla progettazione, dovresti anche favorire gli utenti con visibilità adeguata in ObjC. Ci saranno delle volte in cui dovrai deviare. Per questo motivo, penso che sia spesso meglio limitare gli accessi diretti all'implementazione che dichiara l'ivar (es. Dichiararlo @private ).

Modifica 1:

La maggior parte di noi ha memorizzato come chiamare un accessorio nascosto in modo dinamico (purché conosciamo il nome ...). Nel frattempo, molti di noi non hanno memorizzato come accedere correttamente a ivars che non sono visibili (oltre KVC). La continuazione della classe aiuta , ma introduce vulnerabilità.

Questa soluzione è ovvia:

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

Ora provalo solo con un ivar e senza KVC.


La ragione più importante è il concetto OOP di nascondere le informazioni : se esponi tutto tramite le proprietà e quindi consenti agli oggetti esterni di dare un'occhiata agli interni di un altro oggetto, farai uso di questi interni e quindi complichi la modifica dell'implementazione.

Il guadagno della "prestazione minima" può riassumere rapidamente e diventare un problema. Conosco per esperienza; Lavoro su un'app che porta realmente gli iDevice ai loro limiti e quindi dobbiamo evitare chiamate di metodi non necessarie (ovviamente solo laddove ragionevolmente possibile). Per aiutare con questo obiettivo, evitiamo anche la sintassi del punto poiché rende difficile vedere il numero di chiamate al metodo a prima vista: ad esempio, quante chiamate al metodo ha l'espressione self.image.size.width trigger? Al contrario, puoi dire immediatamente con [[self image] size].width . [[self image] size].width .

Inoltre, con la corretta denominazione di ivar, KVO è possibile senza proprietà (IIRC, non sono un esperto di KVO).


Per me di solito è prestazione. Accedere a un ivar di un oggetto è veloce come accedere a un membro struct in C usando un puntatore alla memoria che contiene tale struct. In effetti, gli oggetti Objective-C sono fondamentalmente strutture C situate nella memoria allocata dinamicamente. Questo di solito è veloce quanto il tuo codice può essere ottenuto, nemmeno il codice di assemblaggio ottimizzato a mano può essere più veloce di così.

L'accesso a un ivar attraverso un getter / setting implica una chiamata al metodo Objective-C, che è molto più lenta (almeno 3-4 volte) di una chiamata di funzione C "normale" e anche una normale chiamata di funzione C sarebbe già più volte più lenta di accedere a un membro struct. A seconda degli attributi della proprietà, l'implementazione setter / getter generata dal compilatore può coinvolgere un'altra chiamata di funzione C alle funzioni objc_getProperty / objc_setProperty , in quanto questi dovranno retain / copy / autorelease gli oggetti in base alle esigenze e eseguire ulteriormente lo spinlocking per atomico proprietà ove necessario. Questo può facilmente diventare molto costoso e non sto parlando di essere più lento del 50%.

Proviamo questo:

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);

Produzione:

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

Questo è 4.28 volte più lento e questo era un int primitivo non atomico, praticamente il migliore dei casi ; la maggior parte degli altri casi è ancora peggio (prova una proprietà atomica NSString * !). Quindi, se riesci a convivere con il fatto che ogni accesso di ivar è 4-5 volte più lento di quanto potrebbe essere, usare le proprietà va bene (almeno quando si tratta di prestazioni), tuttavia, ci sono molte situazioni in cui tale calo di prestazioni è completamente inaccettabile.

Aggiornamento 2015-10-20

Alcune persone sostengono che questo non è un problema del mondo reale, il codice sopra è puramente sintetico e non lo si noterà mai in un'applicazione reale. Va bene, proviamo un campione del mondo reale.

Il seguente codice definisce gli oggetti Account . Un account ha proprietà che descrivono name ( NSString * ), gender ( enum ), age ( unsigned ) del suo proprietario, nonché un saldo ( int64_t ). Un oggetto account ha un metodo init e un metodo compare: Il compare: metodo è definito come: ordini di sesso femminile prima di sesso maschile, ordine di nomi alfabeticamente, ordini di giovani prima del vecchio, saldo di ordini dal basso verso l'alto.

In realtà esistono due classi di account, AccountA e AccountB . Se osservi la loro implementazione, noterai che sono quasi del tutto identici, con un'eccezione: il metodo compare: AccountA oggetti AccountA accedono alle proprie proprietà per metodo (getter), mentre gli oggetti AccountB accedono alle proprie proprietà da ivar. Questa è davvero l'unica differenza! Entrambi accedono alle proprietà dell'altro oggetto per confrontarsi con getter (accedervi con ivar non sarebbe sicuro!) E se l'altro oggetto è una sottoclasse e ha scavalcato il getter?). Si noti inoltre che l'accesso alle proprie proprietà come ivars non interrompe l'incapsulamento (gli ivars non sono ancora pubblici).

La configurazione del test è davvero semplice: crea 1 milione di account casuali, aggiungili a un array e ordinali. Questo è tutto. Naturalmente, ci sono due array, uno per gli oggetti AccountA e uno per gli oggetti AccountB e entrambi gli array sono pieni di account identici (stessa origine dati). Abbiamo tempo quanto tempo ci vuole per ordinare gli array.

Ecco l'output di diverse sessioni che ho fatto ieri:

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

Come puoi vedere, l'ordinamento dell'array di oggetti AccountB è sempre più rapido rispetto all'ordinamento dell'array di oggetti AccountA .

Chiunque sostenga che le differenze di runtime fino a 1,32 secondi non facciano differenza, è meglio non programmare mai la programmazione dell'interfaccia utente. Ad esempio, se desidero modificare l'ordine di ordinamento di una tabella di grandi dimensioni, le differenze di tempo come queste fanno un'enorme differenza per l'utente (la differenza tra un'interfaccia utente accettabile e una più lenta).

Anche in questo caso il codice di esempio è l'unico vero lavoro svolto qui, ma quanto spesso il tuo codice è solo un piccolo ingranaggio di un complicato meccanismo a orologeria? E se ogni ingranaggio rallenta l'intero processo in questo modo, cosa significa per la velocità dell'intero clockwork alla fine? Soprattutto se una fase di lavoro dipende dall'output di un'altra, il che significa che tutte le inefficienze si sommano. La maggior parte delle inefficienze non sono un problema da sole, è la loro somma che diventa un problema per l'intero processo. E tale problema non è nulla che un profiler possa facilmente mostrare perché un profiler riguarda la ricerca di hot spot critici, ma nessuna di queste inefficienze è un hot spot per conto proprio. Il tempo della CPU è appena mediamente diffuso tra di loro, ma ognuno di essi ne ha solo una così piccola frazione, sembra una totale perdita di tempo per ottimizzarlo. Ed è vero, l'ottimizzazione di uno solo di loro non aiuterebbe assolutamente nulla, l'ottimizzazione di tutti loro può aiutare in modo drammatico.

E anche se non pensi in termini di tempo della CPU, perché ritieni che sprecare il tempo della CPU sia del tutto accettabile, dopotutto "è gratis", allora per quanto riguarda i costi di hosting del server causati dal consumo di energia? Che dire del runtime della batteria dei dispositivi mobili? Se si scrive la stessa app mobile due volte (ad esempio un proprio browser web mobile), una volta una versione in cui tutte le classi accedono alle proprie proprietà solo dai getter e una volta che tutte le classi accedono solo a ivars, l'uso costante del primo sarà sicuramente esaurito la batteria è molto più veloce dell'usare la seconda, anche se sono equivalenti funzionali e per l'utente la seconda probabilmente si sentirà anche un po 'più veloce.

Ora ecco il codice per il tuo file main.m (il codice si basa su ARC abilitato e assicurati di usare l'ottimizzazione durante la compilazione per vedere l'effetto completo):

#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

Proprietà vs. variabili di istanza è un compromesso, alla fine la scelta si riduce all'applicazione.

Incapsulamento / Nascondersi delle informazioni Questa è una buona cosa (TM) dal punto di vista del design, le interfacce strette e il collegamento minimo è ciò che rende il software mantenibile e comprensibile. In Obj-C è piuttosto difficile nascondere qualsiasi cosa, ma le variabili di istanza dichiarate nell'implementazione si avvicinano il più possibile.

Prestazioni Mentre "l'ottimizzazione prematura" è una cosa cattiva (TM), scrivere codice con prestazioni scadenti solo perché è possibile almeno altrettanto grave. È difficile arguire che una chiamata al metodo sia più costosa di un carico o di un negozio, e in un codice intensivo di calcolo il costo si somma presto.

In un linguaggio statico con proprietà, come C #, le chiamate a setter / getter possono essere spesso ottimizzate dal compilatore. Tuttavia Obj-C è dinamico e rimuovere tali chiamate è molto più difficile.

Astrazione Un argomento contro le variabili di istanza in Obj-C è stato tradizionalmente la gestione della memoria. Con le variabili di istanza MRC le chiamate a mantenere / rilasciare / autorelease devono essere distribuite su tutto il codice, le proprietà (sintetizzate o meno) mantengono il codice MRC in un unico punto: il principio di astrazione che è una buona cosa (TM). Tuttavia con GC o ARC questo argomento scompare, quindi l'astrazione per la gestione della memoria non è più un argomento contro le variabili di istanza.


Semantica

  • Cosa @property può esprimere che ivars non può: non nonatomic e copy .
  • Quali oggetti possono esprimere che @property non può:

Prestazione

Breve storia: gli ivar sono più veloci, ma non importa per la maggior parte degli usi. nonatomic proprietà non nonatomic non utilizzano i blocchi, ma l'ivar diretto è più veloce perché salta la chiamata degli accessors. Per maggiori dettagli leggere la seguente email da lists.apple.com.

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

Le proprietà influenzano le prestazioni in molti modi:

  1. Come già discusso, l'invio di un messaggio per eseguire un caricamento / un archivio è più lento del semplice caricamento / archiviazione in linea .

  2. L'invio di un messaggio per fare un carico / archivio è anche un po 'più codice che deve essere tenuto in i-cache: anche se il getter / setter ha aggiunto zero istruzioni extra oltre al solo carico / archivio, ci sarebbe una metà solida -Aggiunta di istruzioni extra nel chiamante per impostare il messaggio di invio e gestire il risultato.

  3. L'invio di un messaggio impone una voce per quel selettore da tenere nella cache dei metodi , e quella memoria generalmente si aggira in d-cache. Ciò aumenta il tempo di avvio, aumenta l'utilizzo di memoria statica della tua app e rende gli switch di contesto più dolorosi. Poiché la cache del metodo è specifica per la classe dinamica per un oggetto, questo problema aumenta man mano che usi KVO.

  4. L'invio di un messaggio obbliga tutti i valori della funzione a essere riversati nello stack (o mantenuti in registri salva-calle, il che significa semplicemente fuoriuscire in un altro momento).

  5. L'invio di un messaggio può avere effetti collaterali arbitrari e quindi

    • forza il compilatore a ripristinare tutte le sue ipotesi sulla memoria non locale
    • non può essere issato, affondato, riordinato, coalizzato o eliminato.

  6. In ARC, il risultato di un messaggio inviato verrà sempre mantenuto , sia dal chiamante o dal chiamante, anche per i ritorni +0: anche se il metodo non mantiene / autorizza il suo risultato, il chiamante non lo sa e ha provare ad agire per impedire che il risultato venga automaticamente autorizzato. Questo non può mai essere eliminato perché i messaggi inviati non sono analizzabili staticamente.

  7. In ARC, poiché un metodo setter prende generalmente il suo argomento a +0, non c'è modo di "trasferire" un retain di quell'oggetto (che, come discusso sopra, solitamente ha ARC) nell'avar, quindi il valore deve generalmente essere conservare / rilasciato due volte .

Niente di tutto ciò significa che sono sempre cattivi, ovviamente: ci sono molti buoni motivi per usare le proprietà. Tieni presente che, come molte altre funzionalità linguistiche, non sono gratuite.


John.







ivar