第26条:勿在分类中声明属性
本条要点:(作者总结)
- 把封装数据所用的全部属性都定义在主接口里。
- 在 “class-continuation 分类” 之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
属性是封装数据的方式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了 “class-continuation 分类” 之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
比方说你实现过一个表示个人信息的类,在读过第24 条之后,决定用分类机制将其代码分段。那么你可能会设计一个专门处理交友事务的分类,其中所有方法都与操作某人的朋友列表有关。若是不知道刚才讲的那个问题,可能就会把代表朋友列表的那项属性也放到 Friendship 分类里面去了:
1 @interface EOCPerson : NSObject 2 3 @property (nonatomic, copy, readonly) NSString *firstName; 4 @property (nonatomic, copy, readonly) NSString *lastName; 5 6 - (instancetype)initWithFirstName:(NSString *)firstName 7 andLastName:(NSString *)lastName; 8 9 @end 10 11 @interface EOCPerson (Friendship) 12 13 @property (nonatomic, strong) NSArray *friends; 14 - (BOOL)isFriendsWith:(EOCPerson *)person; 15 16 @end
1 @implementation EOCPerson 2 3 // Methods 4 5 @end 6 7 @implementation EOCPerson (Friendship) 8 9 // Methods 10 11 @end
通过这段代码时,编译器会给出如下警告信息:
1 warning: property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method a method implementation in this category [-Wobjc-property-implementation] 2 3 warning: property 'friends' requires method 'setFriends:' to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]
这段警告信息有点令人费解,意思是说此分类无法合成与 friends 属性相关的实例变量,所以开发者需要在分类中为该属性实现存取方法。此时可以把存取方法声明为 @dynamic,也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。
关联对象能够解决在分类中不能合成实例变量的问题。比方说,我们可以在分类中用下面这段代码实现存取方法:
1 #import "EOCPerson.h" 2 #import <objc/runtime.h> 3 4 @implementation EOCPerson 5 - (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName { 6 self = [super init]; 7 if (self) { 8 // 9 } 10 return self; 11 } 12 13 @end 14 15 static const char *kFriendsPropertyKey = "kFriendsPropertyKey"; 16 17 @implementation EOCPerson (Friendship) 18 19 - (NSArray *)friends { 20 return objc_getAssociatedObject(self, kFriendsPropertyKey); 21 } 22 23 - (void)setFriends:(NSArray *)friends { 24 objc_setAssociatedObject(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 25 } 26 27 - (BOOL)isFriendsWith:(EOCPerson *)person { 28 return NO; 29 } 30 31 @end
这样做可行,但不太理想。要把相似的代码写很多遍,而且在内存管理问题上容易出错,因为我们在为属性实现存取方法时,经常会忘记遵从其内存管理语义。比方说,你可能通过属性特质(attribute)修改了某个属性的内存管理语义。而此时还要记得,在设置方法中也得修改设置关联对象时所用的内存管理语义才行。所以说,尽管这个做法不坏,但是笔者并不推荐。
此外,你可能会选用可变数组来实现 friends 属性所对应的实例变量。若是这样做,就得在设置方法中将传入的数组参数拷贝为可变版本,而这又成为另外一个编码时容易出错的地方。因此,把属性定义在 “主接口”(main interface)中要比定义在分类里清晰得多。
在本例中,正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的 “语法糖” ,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在与扩展类的功能,而非封装数据。
虽说如此,但有时候只读属性还是可以在分类中使用的。比方说,要在 NSCalendar 类中创建分类,以返回包含各个月份名称的字符串数组。由于获取方法并不访问数据,而且属性也不需要由实例变量来实现,所以可像下面这样来实现此分类:
1 #import "NSCalendar+EOC_Additions.h" 2 3 @implementation NSCalendar (EOC_Additions) 4 5 - (NSArray *)eoc_allMonths { 6 if ([self.calendarIdentifier isEqualToString:NSGregorianCalendar]) { 7 return @[@"January", @"February", @"March", @"April", @"May", @"June", @"July", @"August", @"September", @"October", @"November", @"December"]; 8 } else if (/* other calendar identifiers */) { 9 /* return months for other calendars */ 10 } 11 } 12 13 @end
由于实现属性所需的全部方法(在本例中,属性是只读的,所以只需实现一个方法)都已实现,所以不会再为该属性自动合成实例变量了。于是,编译器也就不会发出警告信息。然而,即便这种情况下,也最好不要用属性。属性所要表达的意思是: 类中有数据在支持着它。属性是用来封装数据的。在本例中,应该直接声明一个方法,用以获取月份名称列表:
1 @interface NSCalendar (EOC_Additions) 2 3 //@property (nonatomic, strong, readonly) NSArray *eoc_allMonths; 4 - (NSArray *)eoc_allMonths; 5 6 @end
END