Ziel-C: Eigenschafts- / Instanzvariable in der Kategorie

122

Da ich in Objective-C keine synthetisierte Eigenschaft in einer Kategorie erstellen kann, kann ich den folgenden Code nicht optimieren:

@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

Die Testmethode wird zur Laufzeit mehrmals aufgerufen und ich mache eine Menge Dinge, um das Ergebnis zu berechnen. Normalerweise speichere ich den Wert unter Verwendung einer synthetisierten Eigenschaft beim ersten Aufruf der Methode in einem IVar-Test und gebe diesen IVar beim nächsten Mal zurück. Wie kann ich den obigen Code optimieren?

dhrm
quelle
2
Warum nicht das tun, was Sie normalerweise tun, sondern nur anstelle einer Kategorie die Eigenschaft einer MyClass-Basisklasse hinzufügen? Und um noch weiter zu gehen, führen Sie Ihre schweren Aufgaben im Hintergrund aus und lassen Sie den Prozess eine Benachrichtigung auslösen oder rufen Sie einen Handler für MyClass an, wenn der Prozess abgeschlossen ist.
Jeremy
4
MyClass ist eine aus Core Data generierte Klasse. Wenn ich aber meinen benutzerdefinierten Objektcode in der generierten Klasse verwende, verschwindet dieser, wenn ich die Klasse aus meinen Kerndaten neu generiere. Aus diesem Grund verwende ich eine Kategorie.
Dhrm
1
Vielleicht die Frage annehmen, die am besten für den Titel gilt? ("Eigentum in Kategorie")
hfossli
Warum nicht einfach eine Unterklasse erstellen?
Scott Zhu

Antworten:

124

Die Methode von @ lorean funktioniert (Hinweis: Die Antwort ist jetzt gelöscht) , aber Sie haben nur einen einzigen Speicherplatz. Wenn Sie dies also für mehrere Instanzen verwenden möchten und jede Instanz einen bestimmten Wert berechnen möchte, funktioniert dies nicht.

Glücklicherweise hat die Objective-C-Laufzeit dieses Ding namens Associated Objects , das genau das tun kann, was Sie wollen:

#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
Dave DeLong
quelle
fester Link für
zugehörige
5
@ DaveDeLong Danke für diese Lösung! Nicht sehr schön, aber es funktioniert :)
Dhrm
42
"nicht sehr schön"? Das ist die Schönheit von Objective-C! ;)
Dave DeLong
6
Gute Antwort! Sie können die statische Variable sogar @selector(test)als Schlüssel entfernen
Gabriele Petronella
1
@HotFudgeSunday - denke, es funktioniert schnell: stackoverflow.com/questions/24133058/…
Scott Corscadden
173

.h-Datei

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

.m-Datei

#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

Genau wie eine normale Eigenschaft - zugänglich mit Punktnotation

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

Einfachere Syntax

Alternativ können Sie @selector(nameOfGetter)anstelle eines statischen Zeigerschlüssels Folgendes verwenden:

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

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

Weitere Informationen finden Sie unter https://stackoverflow.com/a/16020927/202451

hfossli
quelle
4
Guter Artikel. Eine Sache zu beachten ist ein Update im Artikel. Update 22. Dezember 2011: Es ist wichtig zu beachten, dass der Schlüssel für die Zuordnung ein ungültiger Zeiger ungültiger * Schlüssel ist, keine Zeichenfolge. Das bedeutet, dass Sie beim Abrufen einer zugehörigen Referenz genau denselben Zeiger an die Laufzeit übergeben müssen. Es würde nicht wie beabsichtigt funktionieren, wenn Sie eine C-Zeichenfolge als Schlüssel verwenden, die Zeichenfolge dann an eine andere Stelle im Speicher kopieren und versuchen, auf die zugehörige Referenz zuzugreifen, indem Sie den Zeiger auf die kopierte Zeichenfolge als Schlüssel übergeben.
Herr Rogers
4
Das brauchst du wirklich nicht @dynamic objectTag;. @dynamicbedeutet, dass der Setter & Getter woanders generiert wird, aber in diesem Fall werden sie genau hier implementiert.
IluTov
@NSAddict wahr! Fest!
Hfossli
1
Wie wäre es mit dem Dealloc von laserUnicorns für die manuelle Speicherverwaltung? Ist das ein Speicherverlust?
Manny
Wird automatisch veröffentlicht stackoverflow.com/questions/6039309/…
hfossli
32

Die gegebene Antwort funktioniert großartig und mein Vorschlag ist nur eine Erweiterung, die vermeidet, zu viel Code auf dem Boilerplate zu schreiben.

Um zu vermeiden, dass wiederholt Getter- und Setter-Methoden für Kategorieeigenschaften geschrieben werden, werden in dieser Antwort Makros eingeführt. Zusätzlich erleichtern diese Makros die Verwendung primitiver Typeneigenschaften wie intoder BOOL.

Traditioneller Ansatz ohne Makros

Traditionell definieren Sie eine Kategorieeigenschaft wie

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

Dann müssen Sie eine Getter- und Setter-Methode implementieren, die ein zugehöriges Objekt und den Get-Selektor als Schlüssel verwendet ( siehe ursprüngliche Antwort ):

#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

Mein vorgeschlagener Ansatz

Mit einem Makro schreiben Sie stattdessen:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Die Makros sind wie folgt definiert:

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

Das Makro CATEGORY_PROPERTY_GET_SETfügt einen Getter und einen Setter für die angegebene Eigenschaft hinzu. Read-only oder Nur - Schreib-Eigenschaften werden die Verwendung CATEGORY_PROPERTY_GETund CATEGORY_PROPERTY_SETjeweils Makro.

Primitive Typen brauchen etwas mehr Aufmerksamkeit

Da primitive Typen keine Objekte sind, enthalten die obigen Makros ein Beispiel für die Verwendung unsigned intals Eigenschaftstyp. Dazu wird der ganzzahlige Wert in ein NSNumberObjekt eingeschlossen. Die Verwendung ist also analog zum vorherigen Beispiel:

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

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

Nach diesem Muster können Sie einfach mehr Makros auch hinzufügen , zu unterstützen signed int, BOOLusw ...

Einschränkungen

  1. Alle Makros werden OBJC_ASSOCIATION_RETAIN_NONATOMICstandardmäßig verwendet.

  2. IDEs wie App Code erkennen derzeit den Namen des Setters nicht, wenn der Name der Eigenschaft umgestaltet wird. Sie müssten es selbst umbenennen.

Lars Blumberg
quelle
1
Vergessen Sie nicht, #import <objc/runtime.h>in der Kategorie .m Datei sonst. Fehler bei der Kompilierung: Die implizite Deklaration der Funktion 'objc_getAssociatedObject' ist in C99 ungültig . stackoverflow.com/questions/9408934/…
Sathe_Nagaraja
7

Verwenden Sie einfach die Bibliothek libextobjc :

h-Datei:

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

m-Datei:

#import <extobjc.h>
@implementation MyClass (Variant)

@synthesizeAssociation (MyClass, test);

@end

Weitere Informationen zu @synthesizeAssociation

Mansurov Ruslan
quelle
Was ist der richtige Weg, um einen Accessor damit zu überschreiben (z. B. faul eine Eigenschaft laden)
David James
3

Nur mit iOS 9 getestet Beispiel: Hinzufügen einer UIView-Eigenschaft zu UINavigationBar (Kategorie)

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
Rikco
quelle
-2

Eine andere mögliche, vielleicht einfachere Lösung, die nicht verwendet Associated Objectswird, besteht darin, eine Variable in der Kategorieimplementierungsdatei wie folgt zu deklarieren:

@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

Der Nachteil dieser Art der Implementierung ist, dass das Objekt nicht als Instanzvariable, sondern als Klassenvariable fungiert. Außerdem können Eigenschaftsattribute nicht zugewiesen werden (wie sie in zugeordneten Objekten wie OBJC_ASSOCIATION_RETAIN_NONATOMIC verwendet werden).

Kernix
quelle
Die Frage fragt nach der Instanzvariablen. Ihre Lösung ist genau wie Lorean
hfossli
1
Ja wirklich?? Wussten Sie, was Sie tun? Sie haben ein Paar von Getter / Setter-Methoden erstellt, um auf eine interne Variable zuzugreifen, die für eine Datei unabhängig ist, ABER für ein Klassenobjekt. Wenn Sie also zwei UIAlertView-Klassenobjekte zuweisen, sind die Objektwerte derselben!
Itachi