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 เมื่อกระบวนการเสร็จสมบูรณ์
Jeremy

4
MyClass เป็นคลาสที่สร้างขึ้นจากข้อมูลหลัก ถ้าฉัน แต่โค้ดออบเจ็กต์ที่กำหนดเองของฉันภายในคลาสที่สร้างขึ้นมันจะหายไปถ้าฉันสร้างคลาสใหม่จากข้อมูลหลักของฉัน ด้วยเหตุนี้ฉันจึงใช้หมวดหมู่
dhrm

1
อาจจะยอมรับคำถามที่ตรงกับชื่อเรื่องมากที่สุด? ("ทรัพย์สินในหมวดหมู่")
hfossli

ทำไมไม่สร้างคลาสย่อยล่ะ
Scott Zhu

คำตอบ:


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

ลิงก์ถาวรสำหรับ Associated Objects: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan

5
@DaveDeLong ขอบคุณสำหรับการแก้ปัญหานี้! ไม่สวยมาก แต่ใช้ได้ :)
dhrm

42
"ไม่สวยมาก"? นี่คือความสวยงามของ Objective-C! ;)
Dave DeLong

6
ตอบโจทย์มาก! คุณยังสามารถกำจัดตัวแปรคงที่โดยใช้@selector(test)เป็นคีย์ได้ตามที่อธิบายไว้ที่นี่: stackoverflow.com/questions/16020918/…
Gabriele Petronella

1
@HotFudgeSunday - คิดว่ามันทำงานได้อย่างรวดเร็ว: stackoverflow.com/questions/24133058/…
Scott Corscadden

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 ธันวาคม 2554: สิ่งสำคัญคือต้องทราบว่าคีย์สำหรับการเชื่อมโยงคือคีย์ void pointer void * ไม่ใช่สตริง นั่นหมายความว่าเมื่อดึงข้อมูลอ้างอิงที่เกี่ยวข้องคุณต้องส่งตัวชี้เดียวกันไปยังรันไทม์ มันจะไม่ทำงานตามที่ตั้งใจไว้หากคุณใช้สตริง C เป็นคีย์จากนั้นคัดลอกสตริงไปยังตำแหน่งอื่นในหน่วยความจำและพยายามเข้าถึงการอ้างอิงที่เกี่ยวข้องโดยส่งตัวชี้ไปยังสตริงที่คัดลอกเป็นคีย์
Mr Rogers

4
คุณไม่จำเป็นต้องใช้ไฟล์@dynamic objectTag;. @dynamicหมายความว่า setter & getter จะถูกสร้างขึ้นที่อื่น แต่ในกรณีนี้จะถูกนำไปใช้ที่นี่
IluTov

@NSAddict จริง! แก้ไขแล้ว!
hfossli

1
วิธีการเกี่ยวกับ dealloc ของ laserUnicorns สำหรับการจัดการหน่วยความจำด้วยตนเอง? นี่คือความทรงจำรั่ว?
Manny

ถูกปล่อยโดยอัตโนมัติstackoverflow.com/questions/6039309/…
hfossli

32

คำตอบที่ให้มาใช้งานได้ดีและข้อเสนอของฉันเป็นเพียงส่วนเสริมที่หลีกเลี่ยงการเขียนโค้ดสำเร็จรูปมากเกินไป

เพื่อหลีกเลี่ยงการเขียนเมธอด getter และ setter ซ้ำ ๆ สำหรับคุณสมบัติหมวดหมู่คำตอบนี้แนะนำมาโคร นอกจากแมโครเหล่านี้ความสะดวกการใช้งานของคุณสมบัติประเภทดั้งเดิมเช่นหรือintBOOL

แนวทางดั้งเดิมที่ไม่มีมาโคร

ตามเนื้อผ้าคุณกำหนดคุณสมบัติประเภทเช่น

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

จากนั้นคุณต้องใช้เมธอด getter และ setter โดยใช้อ็อบเจกต์ที่เกี่ยวข้องและget selectorเป็นคีย์ ( ดูคำตอบเดิม ):

#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จะเพิ่ม getter และ setter สำหรับคุณสมบัติที่กำหนด คุณสมบัติอ่านอย่างเดียวหรือเขียนอย่างเดียวจะใช้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ไม่รู้จักชื่อของ setter เมื่อทำการ refactor ชื่อคุณสมบัติ คุณจะต้องเปลี่ยนชื่อด้วยตัวเอง


1
อย่าลืม#import <objc/runtime.h>อยู่ในหมวดหมู่ไฟล์. m ข้อผิดพลาดในการคอมไพล์: การประกาศฟังก์ชัน 'objc_getAssociatedObject' โดยนัยไม่ถูกต้องใน C99 ปรากฏขึ้น stackoverflow.com/questions/9408934/…
Sathe_Nagaraja

7

เพียงใช้ไลบรารีLibextobjc :

H-ไฟล์:

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

M-ไฟล์:

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

@synthesizeAssociation (MyClass, test);

@end

ข้อมูลเพิ่มเติมเกี่ยวกับ @synthesizeAssociation


อะไรคือวิธีที่ถูกต้องในการแทนที่ accessor ด้วยสิ่งนี้ (เช่น lazy load a property)
David James

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

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


คำถามถามเกี่ยวกับตัวแปรอินสแตนซ์ โซลูชันของคุณก็เหมือนกับ Lorean
hfossli

1
จริงๆ?? คุณรู้ไหมว่าคุณกำลังทำอะไรอยู่? คุณสร้างเมธอด getter / setter คู่เพื่อเข้าถึงตัวแปรภายในซึ่งไม่ขึ้นกับไฟล์ แต่เป็นคลาสอ็อบเจ็กต์นั่นคือถ้าคุณจัดสรรอ็อบเจ็กต์คลาส UIAlertView สองอ็อบเจ็กต์ค่าอ็อบเจ็กต์ของอ็อบเจ็กต์จะเหมือนกัน!
Itachi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.