Objective-C: proprietà / variabile di istanza nella categoria


122

Poiché non riesco a creare una proprietà sintetizzata in una categoria in Objective-C, non so come ottimizzare il seguente codice:

@interface MyClass (Variant)
@property (nonatomic, strong) NSString *test;
@end

@implementation MyClass (Variant)

@dynamic test;

- (NSString *)test {
    NSString *res;
    //do a lot of stuff
    return res;
}

@end

Il metodo di test viene chiamato più volte in fase di esecuzione e sto facendo un sacco di cose per calcolare il risultato. Normalmente usando una proprietà sintetizzata memorizzo il valore in un IVar _test la prima volta che il metodo viene chiamato e restituisco questo IVar la prossima volta. Come posso ottimizzare il codice sopra?


2
Perché non fai quello che fai normalmente, solo invece di una categoria, aggiungi la proprietà a una classe base MyClass? E per andare oltre, esegui le tue attività pesanti in background e fai in modo che il processo attivi una notifica o chiami un gestore per MyClass quando il processo è completo.
Jeremy

4
MyClass è una classe generata da Core Data. Se inserissi il mio codice oggetto personalizzato all'interno della classe generata, scomparirebbe se rigenerassi la classe dai miei dati di base. Per questo motivo, sto usando una categoria.
dhrm

1
Forse accettare la domanda che si applica meglio al titolo? ("Proprietà nella categoria")
hfossli

Perché non creare semplicemente una sottoclasse?
Scott Zhu

Risposte:


124

Il metodo di @ lorean funzionerà (nota: la risposta è ora cancellata) , ma avresti solo un singolo slot di archiviazione. Quindi, se volessi usarlo su più istanze e fare in modo che ciascuna istanza calcoli un valore distinto, non funzionerebbe.

Fortunatamente, il runtime Objective-C ha questa cosa chiamata Oggetti associati che possono fare esattamente quello che vuoi:

#import <objc/runtime.h>

static void *MyClassResultKey;
@implementation MyClass

- (NSString *)test {
  NSString *result = objc_getAssociatedObject(self, &MyClassResultKey);
  if (result == nil) {
    // do a lot of stuff
    result = ...;
    objc_setAssociatedObject(self, &MyClassResultKey, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }
  return result;
}

@end


5
@DaveDeLong Grazie per questa soluzione! Non molto bello ma funziona :)
dhrm

42
"non molto bella"? Questa è la bellezza di Objective-C! ;)
Dave DeLong

6
Bella risposta! Puoi anche eliminare la variabile statica usando @selector(test)come chiave, come spiegato qui: stackoverflow.com/questions/16020918/…
Gabriele Petronella

1
@HotFudgeSunday - credo che funziona in rapida: stackoverflow.com/questions/24133058/...
Scott Corscadden

174

.h file

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

.m file

#import <objc/runtime.h>

static void * LaserUnicornPropertyKey = &LaserUnicornPropertyKey;

@implementation NSObject (LaserUnicorn)

- (LaserUnicorn *)laserUnicorn {
    return objc_getAssociatedObject(self, LaserUnicornPropertyKey);
}

- (void)setLaserUnicorn:(LaserUnicorn *)unicorn {
    objc_setAssociatedObject(self, LaserUnicornPropertyKey, unicorn, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
}

@end

Proprio come una normale proprietà, accessibile con la notazione a punti

NSObject *myObject = [NSObject new];
myObject.laserUnicorn = [LaserUnicorn new];
NSLog(@"Laser unicorn: %@", myObject.laserUnicorn);

Sintassi più semplice

In alternativa puoi usare @selector(nameOfGetter)invece di creare un tasto puntatore statico in questo modo:

- (LaserUnicorn *)laserUnicorn {
    return objc_getAssociatedObject(self, @selector(laserUnicorn));
}

- (void)setLaserUnicorn:(LaserUnicorn *)unicorn {
    objc_setAssociatedObject(self, @selector(laserUnicorn), unicorn, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
}

Per maggiori dettagli, vedere https://stackoverflow.com/a/16020927/202451


4
Buon articolo. Una cosa da notare è un aggiornamento nell'articolo. Aggiornamento del 22 dicembre 2011: è importante notare che la chiave per l'associazione è un puntatore vuoto void * chiave, non una stringa. Ciò significa che quando si recupera un riferimento associato, è necessario passare lo stesso identico puntatore al runtime. Non funzionerebbe come previsto se si usasse una stringa C come chiave, quindi si copiasse la stringa in un altro punto della memoria e si tentasse di accedere al riferimento associato passando il puntatore alla stringa copiata come chiave.
Mr Rogers

4
Non hai davvero bisogno del file @dynamic objectTag;. @dynamicsignifica che il setter e il getter verranno generati da qualche altra parte, ma in questo caso sono implementati proprio qui.
IluTov

@NSAddict true! Fisso!
hfossli

1
Che ne dici del dealloc di laserUnicorns per la gestione manuale della memoria? È una perdita di memoria?
Manny

Viene rilasciato automaticamente stackoverflow.com/questions/6039309/...
hfossli

32

La risposta fornita funziona alla grande e la mia proposta è solo un'estensione che evita di scrivere troppo codice boilerplate.

Per evitare di scrivere ripetutamente metodi getter e setter per le proprietà delle categorie, questa risposta introduce delle macro. Inoltre, queste macro facilitano l'uso di proprietà di tipo primitivo come into BOOL.

Approccio tradizionale senza macro

Tradizionalmente si definisce una proprietà di categoria come

@interface MyClass (Category)
@property (strong, nonatomic) NSString *text;
@end

Quindi è necessario implementare un metodo getter e setter utilizzando un oggetto associato e il selettore get come chiave ( vedere la risposta originale ):

#import <objc/runtime.h>

@implementation MyClass (Category)
- (NSString *)text{
    return objc_getAssociatedObject(self, @selector(text));
}

- (void)setText:(NSString *)text{
    objc_setAssociatedObject(self, @selector(text), text, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

Il mio approccio suggerito

Ora, usando una macro scriverai invece:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Le macro sono definite come segue:

#import <objc/runtime.h>

#define CATEGORY_PROPERTY_GET(type, property) - (type) property { return objc_getAssociatedObject(self, @selector(property)); }
#define CATEGORY_PROPERTY_SET(type, property, setter) - (void) setter (type) property { objc_setAssociatedObject(self, @selector(property), property, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
#define CATEGORY_PROPERTY_GET_SET(type, property, setter) CATEGORY_PROPERTY_GET(type, property) CATEGORY_PROPERTY_SET(type, property, setter)

#define CATEGORY_PROPERTY_GET_NSNUMBER_PRIMITIVE(type, property, valueSelector) - (type) property { return [objc_getAssociatedObject(self, @selector(property)) valueSelector]; }
#define CATEGORY_PROPERTY_SET_NSNUMBER_PRIMITIVE(type, property, setter, numberSelector) - (void) setter (type) property { objc_setAssociatedObject(self, @selector(property), [NSNumber numberSelector: property], OBJC_ASSOCIATION_RETAIN_NONATOMIC); }

#define CATEGORY_PROPERTY_GET_UINT(property) CATEGORY_PROPERTY_GET_NSNUMBER_PRIMITIVE(unsigned int, property, unsignedIntValue)
#define CATEGORY_PROPERTY_SET_UINT(property, setter) CATEGORY_PROPERTY_SET_NSNUMBER_PRIMITIVE(unsigned int, property, setter, numberWithUnsignedInt)
#define CATEGORY_PROPERTY_GET_SET_UINT(property, setter) CATEGORY_PROPERTY_GET_UINT(property) CATEGORY_PROPERTY_SET_UINT(property, setter)

La macro CATEGORY_PROPERTY_GET_SETaggiunge un getter e un setter per la proprietà data. Le proprietà di sola lettura o di sola scrittura utilizzeranno rispettivamente la macro CATEGORY_PROPERTY_GETe CATEGORY_PROPERTY_SET.

I tipi primitivi hanno bisogno di un po 'più di attenzione

Poiché i tipi primitivi non sono oggetti, le macro precedenti contengono un esempio da utilizzare unsigned intcome tipo di proprietà. Lo fa avvolgendo il valore intero in un NSNumberoggetto. Quindi il suo utilizzo è analogo all'esempio precedente:

@interface ...
@property unsigned int value;
@end

@implementation ...
CATEGORY_PROPERTY_GET_SET_UINT(value, setValue:)
@end

A seguito di questo modello, si può semplicemente aggiungere più macro per supportare anche signed int, BOOL, ecc ...

limitazioni

  1. Tutte le macro vengono utilizzate OBJC_ASSOCIATION_RETAIN_NONATOMICper impostazione predefinita.

  2. IDE come App Code attualmente non riconoscono il nome del setter durante il refactoring del nome della proprietà. Dovresti rinominarlo da solo.


1
non dimenticare di #import <objc/runtime.h>nella categoria file .m altrimenti. errore in fase di compilazione: la dichiarazione implicita della funzione "objc_getAssociatedObject" non è valida in C99 . stackoverflow.com/questions/9408934/...
Sathe_Nagaraja


3

Testato solo con iOS 9 Esempio: aggiunta di una proprietà UIView a UINavigationBar (categoria)

UINavigationBar + helper.h

#import <UIKit/UIKit.h>

@interface UINavigationBar (Helper)
@property (nonatomic, strong) UIView *tkLogoView;
@end

UINavigationBar + Helper.m

#import "UINavigationBar+Helper.h"
#import <objc/runtime.h>

#define kTKLogoViewKey @"tkLogoView"

@implementation UINavigationBar (Helper)

- (void)setTkLogoView:(UIView *)tkLogoView {
    objc_setAssociatedObject(self, kTKLogoViewKey, tkLogoView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)tkLogoView {
    return objc_getAssociatedObject(self, kTKLogoViewKey);
}

@end

-2

Un'altra possibile soluzione, forse più semplice, che non si utilizza Associated Objectsè dichiarare una variabile nel file di implementazione della categoria come segue:

@interface UIAlertView (UIAlertViewAdditions)

- (void)setObject:(id)anObject;
- (id)object;

@end


@implementation UIAlertView (UIAlertViewAdditions)

id _object = nil;

- (id)object
{
    return _object;
}

- (void)setObject:(id)anObject
{
    _object = anObject;
}
@end

Lo svantaggio di questo tipo di implementazione è che l'oggetto non funziona come una variabile di istanza, ma piuttosto come una variabile di classe. Inoltre, gli attributi di proprietà non possono essere assegnati (come quelli utilizzati negli oggetti associati come OBJC_ASSOCIATION_RETAIN_NONATOMIC)


La domanda chiede sulla variabile di istanza. La tua soluzione è proprio come Lorean
hfossli

1
Veramente?? Sapevi cosa stai facendo? Hai creato una coppia di metodi getter / setter per accedere a una variabile interna che è indipendente per un file MA un oggetto di classe, cioè, se assegni due oggetti di classe UIAlertView, i loro valori oggetto sono gli stessi!
Itachi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.