Effective OC 2.0是一本非常赞的书,如果让我评分,我绝对给10分。
之前读了一遍Effective OC 2.0这本书,现在的想法是将里面的建议实践到工程中,同时将一些使用心得总结成博客。
本文说的是第7条:在对象内部尽量直接访问实例变量
首先要说明:
类中的成员变量在本文称之为实例变量,用@property+@synthesize可以将该变量声明为属性,实际上就是要求编译器自动为其生成accessor(setter/getter)方法,accessor方法可以被覆写。
访问实例变量有3种方法:
(1)调用属性的setter/getter方法
(2)使用dot syntax,实际上就是在调用setter/getter方法
(3)直接通过实例变量访问,此时setter/getter方法会被绕过
假设我们有一个Wrestler类:
@interface Wrestler : NSObject
@property (copy, nonatomic) NSString *name; // 将name声明为属性
- (void)smell;
@end
@implementation Wrestler
@synthesize name = _name; // 属性name可以使用实例变量_name直接访问
- (void)setName:(NSString *)aName {
NSLog(@"Set name");
_name = [aName copy];
}
- (NSString *)name {
NSLog(@"Get name");
return [_name copy];
}
- (void)smell {
NSLog(@"*** Smelling ***");
// 使用dot syntax访问实例变量
NSLog(@"%@", self.name);
// 直接调用属性的getter方法
NSLog(@"%@", [self name]);
// 直接访问实例变量
NSLog(@"%@", _name);
}
@end
测试代码如下:
Wrestler *wrestler = [[Wrestler alloc] init];
// 直接调用属性的setter方法
[wrestler setName:@"John"];
// 使用dot syntax访问name属性
wrestler.name = @"Cena";
[wrestler smell];
2014-05-04 21:09:44.755 AccessorDemo[1021:303] Set name
2014-05-04 21:09:44.756 AccessorDemo[1021:303] Set name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] *** Smelling ***
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Get name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Cena
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Get name
2014-05-04 21:09:44.757 AccessorDemo[1021:303] Cena
2014-05-04 21:09:44.758 AccessorDemo[1021:303] Cena
由上面的结果我们可以得出如下结论:
1.使用属性访问实例变量,需要向self发送消息(即调用accessor方法),从而有一个消息转发的过程。而直接访问实例变量则绕过了这一过程,无疑后者更快。
2.注意到属性有多种特性修饰,例如strong,weak,copy,retain,nonatomic。
在调用accessor方法时会根据其特性进行定制,例如对于copy特性的name,accessor方法类似于下面的形式:
- (void)setName:(NSString *)aName {
NSLog(@"Set name");
_name = [aName copy];
}
- (NSString *)name {
NSLog(@"Get name");
return [_name copy];
}
如果使用_name直接访问实例变量,那么上面的copy过程便会被绕过。这就绕过了为属性定义的所谓“内存管理语义”,这明显不好。
尤其如果开发者重写了属性的accessor方法,那么开发者额外定制的内容也得不到执行。
折中的方法是,访问实例变量时直接访问,设置实例变量值时调用属性的setter方法。这样既保证效率又保证了内存管理语义得到执行。
某些情况下,直接访问实例变量和使用属性的accessor方法访问只能二取其一。
下面列举三种情形:
将上面的setter方法修改如下:
- (void)setName:(NSString *)aName {
NSLog(@"Set name");
// _name = [aName copy];
self.name = aName;
}
原因:在setter方法中调用setter方法会不断嵌套调用,最终导致程序崩溃。
所以自己重写属性setter方法时就不要犯这种低级错误了。
getter方法同理。
下面举一个例子。
我们写一个Wrestler的子类Cena,该类继承了属性name并重写了其setter方法,该方法会先检验名字后缀是否为Cena,否则抛出异常。
@interface Cena : Wrestler
- (instancetype)initWithName:(NSString *)aName;
- (void)wrestle;
@end
@implementation Cena
@synthesize name = _name;
- (instancetype)initWithName:(NSString *)aName {
self = [super init];
if (self) {
NSLog(@"self.name = aName");
self.name = aName;
}
return self;
}
- (void)wrestle {
NSLog(@"I'm %@, U can't see me", self.name);
}
- (void)setName:(NSString *)aName {
if (![aName hasSuffix:@"Cena"]) {
[NSException raise:NSInvalidArgumentException format:@"last name must be Cena"];
}
_name = [aName copy];
}
@end
测试代码如下:
Cena *cena = [[Cena alloc] initWithName:@"John Cena"];
[cena wrestle];
但是某位老兄在父类Wrestler的init方法中将name初始化为空白字符串@"",代码如下:
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"self.name = empty string");
self.name = @"";
}
return self;
}
原因:self.name = @"";调用子类中覆写的name的setter方法,空白字符串明显没有@"Cena"后缀,从而抛出异常。
解决方法,在Wrestler方法中直接访问实例变量,不要调用setter方法:
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"self.name = empty string");
// self.name = @"";
_name = @"";
}
return self;
}
总结,绝不要在父类初始化中调用setter/getter方法,因为它们可能被子类重写,而子类又在它们身上添加了一些较为严苛的要求。
我想这应该可以回答这篇博客中提出的问题:不要在init和dealloc函数中使用accessor
例如我们在Cena类中添加以下属性和方法:
@property (strong, nonatomic) NSNumber *chamCount;
- (void)showChampionCount;
- (NSNumber *)chamCount {
if (!_chamCount) {
_chamCount = @13;
}
return _chamCount;
}
- (void)showChampionCount {
NSLog(@"Champion count = %d", [_chamCount integerValue]);
}
测试代码:
Cena *cena = [[Cena alloc] initWithName:@"John Cena"];
[cena showChampionCount];
输出和调试结果如下:
2014-05-04 22:04:51.722 AccessorDemo[1798:303] Champion count = 0
(lldb) po _chamCount
nil
解决方法,使用getter方法访问:
- (void)showChampionCount {
// NSLog(@"Champion count = %d", [_chamCount integerValue]);
NSLog(@"Champion count = %d", [self.chamCount integerValue]);
}
2014-05-04 22:07:16.475 AccessorDemo[1813:303] Champion count = 13
1.在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
2.在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
3.使用Lazy Initialization配置的数据,应该通过属性来读取数据。
4.不要在setter/getter方法中调用setter/getter方法
5.如果非得用直接访问实例变量的方法,那么尽量保持其内存管理语义得到实施(如copy)。