第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

posted @ 2017-08-03 20:23  鳄鱼不怕牙医不怕  阅读(257)  评论(0编辑  收藏  举报