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