Objective-C: Variabel properti / instance dalam kategori

122

Karena saya tidak dapat membuat properti yang disintesis dalam Kategori di Objective-C, saya tidak tahu cara mengoptimalkan kode berikut:

@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

Metode pengujian dipanggil beberapa kali saat runtime dan saya melakukan banyak hal untuk menghitung hasilnya. Biasanya menggunakan properti yang disintesis, saya menyimpan nilai dalam IVar _test saat pertama kali metode dipanggil, dan hanya mengembalikan IVar ini lain kali. Bagaimana saya bisa mengoptimalkan kode di atas?

dhrm
sumber
2
Mengapa tidak melakukan apa yang biasanya Anda lakukan, hanya sebagai pengganti kategori, menambahkan properti ke kelas dasar MyClass? Dan untuk melangkah lebih jauh, lakukan tugas berat Anda di latar belakang dan biarkan proses mengaktifkan notifikasi atau panggil beberapa penangan untuk MyClass saat proses selesai.
Jeremy
4
MyClass adalah kelas yang dihasilkan dari Core Data. Jika saya tetapi kode objek khusus saya di dalam kelas yang dihasilkan, itu akan hilang jika saya membuat ulang kelas dari Data Inti saya. Karena itu, saya menggunakan kategori.
dhrm
1
Mungkin menerima pertanyaan yang paling sesuai dengan judul? ("Properti dalam kategori")
hfossli
Mengapa tidak membuat subclass saja?
Scott Zhu

Jawaban:

124

Metode @ lorean akan berfungsi (catatan: jawaban sekarang dihapus) , tetapi Anda hanya memiliki satu slot penyimpanan. Jadi, jika Anda ingin menggunakan ini pada beberapa instance dan setiap instance menghitung nilai yang berbeda, itu tidak akan berfungsi.

Untungnya, runtime Objective-C memiliki hal yang disebut Associated Objects yang dapat melakukan apa yang Anda inginkan:

#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
sumber
tautan tetap untuk Objek Terkait: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan
5
@DaveDeLong Terima kasih atas solusi ini! Tidak terlalu indah tapi berhasil :)
dhrm
42
"tidak terlalu cantik"? Inilah keindahan Objective-C! ;)
Dave DeLong
6
Jawaban yang bagus! Anda bahkan dapat menghilangkan variabel statis menggunakan @selector(test)sebagai kunci, seperti yang dijelaskan di sini: stackoverflow.com/questions/16020918/…
Gabriele Petronella
1
@HotFudgeSunday - pikir itu berfungsi dengan cepat: 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

Sama seperti properti biasa - dapat diakses dengan notasi titik

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

Sintaks yang lebih mudah

Sebagai alternatif, Anda dapat menggunakan @selector(nameOfGetter)alih-alih membuat kunci penunjuk statis seperti:

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

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

Untuk detail lebih lanjut, lihat https://stackoverflow.com/a/16020927/202451

hfossli
sumber
4
Artikel bagus. Satu hal yang perlu diperhatikan adalah pembaruan pada artikel. Pembaruan 22 Desember 2011: Penting untuk dicatat bahwa kunci untuk asosiasi adalah kunci void pointer void *, bukan string. Artinya, saat mengambil referensi terkait, Anda harus meneruskan penunjuk yang sama persis ke runtime. Ini tidak akan berfungsi sebagaimana mestinya jika Anda menggunakan string C sebagai kunci, lalu menyalin string ke tempat lain dalam memori dan mencoba mengakses referensi terkait dengan meneruskan penunjuk ke string yang disalin sebagai kunci.
Tuan Rogers
4
Anda benar-benar tidak membutuhkan file @dynamic objectTag;. @dynamicberarti penyetel & pengambil akan dibuat di tempat lain, tetapi dalam hal ini mereka diterapkan di sini.
IluTov
@NSAdict benar! Tetap!
hfossli
1
Bagaimana tentang dealloc laserUnicorns untuk manajemen memori manual? Apakah ini kebocoran memori?
Manny
Dirilis secara otomatis stackoverflow.com/questions/6039309/…
hfossli
32

Jawaban yang diberikan bekerja dengan baik dan proposal saya hanyalah perpanjangan dari itu yang menghindari penulisan terlalu banyak kode boilerplate.

Untuk menghindari penulisan berulang kali metode pengambil dan penyetel untuk properti kategori, jawaban ini memperkenalkan makro. Selain itu, makro ini memudahkan penggunaan properti tipe primitif seperti intatau BOOL.

Pendekatan tradisional tanpa makro

Secara tradisional Anda mendefinisikan properti kategori seperti

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

Kemudian Anda perlu menerapkan metode pengambil dan penyetel menggunakan objek terkait dan pemilih get sebagai kuncinya ( lihat jawaban asli ):

#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

Pendekatan yang saya sarankan

Sekarang, dengan menggunakan makro Anda akan menulis:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Makro ditentukan sebagai berikut:

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

Makro CATEGORY_PROPERTY_GET_SETmenambahkan pengambil dan penyetel untuk properti tertentu. Properti read-only atau write-only akan menggunakan makro CATEGORY_PROPERTY_GETdan CATEGORY_PROPERTY_SETmasing-masing.

Tipe primitif membutuhkan lebih banyak perhatian

Karena tipe primitif bukan objek, makro di atas berisi contoh untuk digunakan unsigned intsebagai tipe properti. Ia melakukannya dengan membungkus nilai integer ke dalam sebuah NSNumberobjek. Jadi penggunaannya analog dengan contoh sebelumnya:

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

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

Mengikuti pola ini, Anda hanya dapat menambahkan lebih banyak macro untuk juga mendukung signed int, BOOL, dll ...

Batasan

  1. Semua makro digunakan OBJC_ASSOCIATION_RETAIN_NONATOMICsecara default.

  2. IDE seperti Kode Aplikasi saat ini tidak mengenali nama penyetel saat memfaktorkan ulang nama properti. Anda perlu mengganti namanya sendiri.

Lars Blumberg
sumber
1
jangan lupa #import <objc/runtime.h>di file .m kategori jika tidak. kesalahan waktu kompilasi: Deklarasi implisit dari fungsi 'objc_getAssociatedObject' tidak valid di C99 muncul. stackoverflow.com/questions/9408934/…
Sathe_Nagaraja
7

Cukup gunakan libextobjc perpustakaan :

file-h:

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

m-file:

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

@synthesizeAssociation (MyClass, test);

@end

Selengkapnya tentang @synthesizeAssociation

Mansurov Ruslan
sumber
Apa cara yang benar untuk mengganti pengakses dengan ini (misalnya pemuatan lambat properti)
David James
3

Hanya diuji dengan iOS 9 Contoh: Menambahkan properti UIView ke UINavigationBar (Category)

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
sumber
-2

Solusi lain yang mungkin, mungkin lebih mudah, yang tidak digunakan Associated Objectsadalah mendeklarasikan variabel dalam file implementasi kategori sebagai berikut:

@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

Kelemahan dari implementasi semacam ini adalah bahwa objek tidak berfungsi sebagai variabel instan, melainkan sebagai variabel kelas. Selain itu, atribut properti tidak dapat ditetapkan (seperti yang digunakan di Objek Terkait seperti OBJC_ASSOCIATION_RETAIN_NONATOMIC)

kernix
sumber
Pertanyaannya menanyakan tentang variabel instan. Solusi Anda seperti Lorean
hfossli
1
Betulkah?? Tahukah Anda apa yang Anda lakukan? Anda membuat pasangan metode pengambil / penyetel untuk mengakses variabel internal yang independen untuk file TETAPI objek kelas, yaitu, jika Anda mengalokasikan dua objek kelas UIAlertView, nilai objeknya sama!
Itachi