Effective OC 2.0读书笔记 第7条:在对象内部尽可能直接访问实例变量
Effective OC 2.0是一本非常赞的书,如果让我评分,我绝对给10分。
之前读了一遍Effective OC 2.0这本书,现在的想法是将里面的建议实践到工程中,同时将一些使用心得总结成博客。
本文说的是第7条:在对象内部尽量直接访问实例变量
首先要说明:
类中的成员变量在本文称之为实例变量,用@property+@synthesize可以将该变量声明为属性,实际上就是要求编译器自动为其生成accessor(setter/getter)方法,accessor方法可以被覆写。
比较“通过属性的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方法访问只能二取其一。
下面列举三种情形:
1.不要在setter方法中调用setter方法或使用dot syntax
将上面的setter方法修改如下:
- (void)setName:(NSString *)aName { NSLog(@"Set name"); // _name = [aName copy]; self.name = aName; }
跑一跑,控制台不停输出Set name,崩溃。
原因:在setter方法中调用setter方法会不断嵌套调用,最终导致程序崩溃。
所以自己重写属性setter方法时就不要犯这种低级错误了。
getter方法同理。
2.不要在init和dealloc方法中调用accessor方法
下面举一个例子。
我们写一个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
3.在Lazy Initialization中必须通过getter方法访问属性
例如我们在Cena类中添加以下属性和方法:
@property (strong, nonatomic) NSNumber *chamCount; - (void)showChampionCount; - (NSNumber *)chamCount { if (!_chamCount) { _chamCount = @13; } return _chamCount; } - (void)showChampionCount { NSLog(@"Champion count = %d", [_chamCount integerValue]); }
这里的chamCount属性的getter方法使用了Lazy Initialization,只有用到该属性才会初始化。
测试代码:
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
原因:直接访问_chamCount绕过了属性的getter方法,使其没有初始化。
解决方法,使用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)。