Objective-C: переменная свойства / экземпляра в категории

122

Поскольку я не могу создать синтезированное свойство в категории в Objective-C, я не знаю, как оптимизировать следующий код:

@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

Тест-метод вызывается несколько раз во время выполнения , и я делаю много вещей , чтобы вычислить результат. Обычно, используя синтезированное свойство, я сохраняю значение в IVar _test при первом вызове метода и просто возвращаю этот IVar в следующий раз. Как я могу оптимизировать приведенный выше код?

ОУЛР
источник
2
Почему бы не сделать то, что вы обычно делаете, только вместо категории добавить свойство в базовый класс MyClass? И чтобы пойти дальше, выполняйте свои тяжелые вещи в фоновом режиме и пусть процесс запускает уведомление или вызывает некоторый обработчик для MyClass, когда процесс завершен.
Джереми
4
MyClass - это созданный класс из Core Data. Если я сохраню свой собственный объектный код внутри сгенерированного класса, он исчезнет, ​​если я регенерирую класс из своих основных данных. Из-за этого я использую категорию.
dhrm 04
1
Может быть, примите вопрос, который лучше всего подходит к названию? («Недвижимость в категории»)
hfossli
Почему бы просто не создать подкласс?
Скотт Чжу

Ответы:

124

Метод @lorean будет работать (примечание: ответ теперь удален) , но у вас будет только один слот для хранения. Поэтому, если вы хотите использовать это в нескольких экземплярах и каждый экземпляр вычисляет отдельное значение, это не сработает.

К счастью, в среде выполнения Objective-C есть такая штука, называемая Associated Objects, которая может делать именно то, что вы хотите:

#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
Дэйв Делонг
источник
фиксированная ссылка для связанных объектов: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan
5
@DaveDeLong Спасибо за это решение! Не очень красиво, но работает :)
dhrm 04
42
"не очень красиво"? В этом вся прелесть Objective-C! ;)
Дэйв Делонг
6
Отличный ответ! Вы даже можете избавиться от статической переменной, используя @selector(test)в качестве ключа, как описано здесь: stackoverflow.com/questions/16020918/…
Габриэле Петронелла
1
@HotFudgeSunday - думаю, он работает быстро: stackoverflow.com/questions/24133058/…
Скотт Корскадден,
174

.h-файл

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

.m-файл

#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

Как обычное свойство - доступно через точечную нотацию

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

Более простой синтаксис

В качестве альтернативы вы можете использовать @selector(nameOfGetter)вместо создания ключа статического указателя, например:

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

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

Подробнее см. Https://stackoverflow.com/a/16020927/202451.

хфоссли
источник
4
Хорошая статья. Стоит отметить обновление статьи. Обновление от 22 декабря 2011 г. Важно отметить, что ключ для ассоциации - это ключ void *, а не строка. Это означает, что при получении связанной ссылки вы должны передать точно такой же указатель во время выполнения. Это не будет работать должным образом, если вы использовали строку C в качестве ключа, затем скопировали строку в другое место в памяти и попытались получить доступ к связанной ссылке, передав указатель на скопированную строку в качестве ключа.
Мистер Роджерс
4
Вам действительно не нужен @dynamic objectTag;. @dynamicозначает, что сеттер и геттер будут сгенерированы где-то еще, но в этом случае они реализованы прямо здесь.
IluTov
@NSAddict правда! Исправлена!
hfossli
1
Как насчет деаллока laserUnicorns для ручного управления памятью? Это утечка памяти?
Мэнни
Выпущено
32

Данный ответ отлично работает, и мое предложение - это просто его расширение, позволяющее избежать написания слишком большого количества шаблонного кода.

Чтобы избежать многократного написания методов получения и установки для свойств категории, в этом ответе представлены макросы. Кроме того, эти макросы упрощают использование свойств примитивных типов, таких как intили BOOL.

Традиционный подход без макросов

Традиционно вы определяете свойство категории, например

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

Затем вам нужно реализовать метод получения и установки, используя связанный объект и селектор получения в качестве ключа ( см. Исходный ответ ):

#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

Предлагаемый мной подход

Теперь, используя макрос, вы вместо этого напишите:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Макросы определены следующим образом:

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

Макрос CATEGORY_PROPERTY_GET_SETдобавляет геттер и сеттер для данного свойства. Только для чтения или только для записи свойства будет использовать CATEGORY_PROPERTY_GETи CATEGORY_PROPERTY_SETмакро соответственно.

Примитивные типы требуют немного большего внимания

Поскольку примитивные типы не являются объектами, указанные выше макросы содержат пример использования unsigned intв качестве типа свойства. Это делается путем обертывания целочисленного значения в NSNumberобъект. Таким образом, его использование аналогично предыдущему примеру:

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

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

После этой модели, вы можете просто добавить больше макросов также поддержку signed int, BOOLи т.д. ...

Ограничения

  1. OBJC_ASSOCIATION_RETAIN_NONATOMICПо умолчанию используются все макросы .

  2. IDE, такие как App Code , в настоящее время не распознают имя установщика при рефакторинге имени свойства. Вам нужно будет переименовать его самостоятельно.

Ларс Блумберг
источник
1
#import <objc/runtime.h>в противном случае не забудьте добавить в категорию .m файл. Ошибка времени компиляции: неявное объявление функции objc_getAssociatedObject недопустимо в C99 . stackoverflow.com/questions/9408934/…
Sathe_Nagaraja
7

Просто используйте библиотеку libextobjc :

ч-файл:

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

м-файл:

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

@synthesizeAssociation (MyClass, test);

@end

Подробнее о @synthesizeAssociation

Мансуров Руслан
источник
Каков правильный способ переопределить аксессор с помощью этого (например, ленивая загрузка свойства)
Дэвид Джеймс,
3

Протестировано только с iOS 9 Пример: добавление свойства UIView в UINavigationBar (Категория)

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
источник
-2

Другое возможное решение, возможно, более простое, которое не используется, Associated Objects- объявить переменную в файле реализации категории следующим образом:

@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

Обратной стороной такой реализации является то, что объект функционирует не как переменная экземпляра, а как переменная класса. Кроме того, атрибуты свойств не могут быть назначены (например, используемые в связанных объектах, таких как OBJC_ASSOCIATION_RETAIN_NONATOMIC)

kernix
источник
Вопрос касается переменной экземпляра. Ваше решение точно такое же, как у Lorean
hfossli
1
В самом деле?? Вы знали, что делаете? Вы создали пару методов getter / setter для доступа к внутренней переменной, которая не зависит от файла, НО объект класса, то есть, если вы выделяете два объекта класса UIAlertView, их значения объектов совпадают!
Итачи