第7条:在对象内部尽量直接访问实例变量

  本条要点:(作者总结)

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写。
  • 在初始化方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用懒加载初始化技术配置某份数据,这中情况下,需要通过属性来读取数据。

  在对象之外访问实例变量时,总是应该通过属性来做,然而在对象内部访问实例变量时又该如何呢?Objective-C 的开发者们一直在激烈争论这个问题。有的人认为,无论什么情况,都应该通过属性来访问实例变量;也有人说,“通过属性访问”与“直接访问”这两种做法应该搭配着用。除了几种特殊情况之外,笔者强烈建议大家在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。

  请看下面这个类:

 1 @interface EOCPerson : NSObject
 2 
 3 @property (nonatomic, copy) NSString *firstName;
 4 @property (nonatomic, copy) NSString *lastName;
 5 
 6 // Convenience for firstName + "" + lastName
 7 - (NSString *)fullName;
 8 - (void)setFullName:(NSString *)fullName;
 9 
10 @end

  fullName 与 setFullName 这两个 “便捷方法” 可以这样来实现:

 1 - (NSString *)fullName {
 2     return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
 3 }
 4 
 5 // The following assumes all full name have exactly 2 parts. The method could be rewritten to support more exotic name
 6 - (void)setFullName:(NSString *)fullName {
 7     NSArray *components = [fullName componentsSeparatedByString:@" "];
 8     self.firstName = [components objectAtIndex:0];
 9     self.lastName = [components objectAtIndex:1];
10 }

  在 fullName 的获取方法与设置方法中,我们使用 “点语法”,通过存取方法来访问相关实例变量。现在假设重写这个方法,不经由存取方法,而是直接访问实例变量:

1 - (NSString *)fullName {
2     return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
3 }
4 
5 - (void)setFullName:(NSString *)fullName {
6     NSArray *components = [fullName componentsSeparatedByString:@" "];
7     _firstName = [components objectAtIndex:0];
8     _lastName = [components objectAtIndex:1];
9 }

  这两种写法有几个区别。

  • 由于不经过 Objective-C  的 “方法派发”(method dispatch)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在 ARC 下直接访问一个声明为 copy 的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发“键值观察”(Key-Value Observing,KVO)(一种通知机制,当某个对象属性改变时,可通知其他对象。)通知。这样做是否会产生问题,还取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点”(breakpoint),监控该属性的调用者及其访问机。

  有一种合理的折中方案,那就是:在写入实例变量时,通过其“设置方法”来做,而在读取实例变量时,则直接访问之。此方法既能提高读取操作的速度,又能控制对属性的写入操作。之所以要通过“设置方法”来写入实例变量,其首要原因在于,这样做能够确保相关属性的“内存管理语义”得以贯彻。但是,选用这种做法时,需注意几个问题。

  第一个要注意的地方是,在初始化方法中应该如何设置属性值。这中情况下总是应该直接访问实例变量,因为子类可能会“覆写”(override)设置方法。假设 EOCPerson 有一个子类叫做 EOCSmithPerson,这个子类专门表示那些姓“Smith”的人。该子类可能会覆写 lastName 属性所对应的设置方法:

1 - (void)setLastName:(NSString *)lastName {
2     if (![lastName isEqualToString:@"Smith"]) {
3         [NSException raise:NSInvalidArgumentException format:@"Last name must be Smith"];
4     }
5     self.lastName = lastName;
6 }

  在基类 EOCPerson 的默认初始化方法中,可能会将姓氏设为字符串。此时若是通过“设置方法”来做,那么调用的将会是子类的设置方法,从而抛出异常。但是,某些情况下却又必须在初始化方法中调用设置方法:如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用“设置方法”了。

  另外一个需要注意的问题是 “惰性初始化”(lazy initialization)。在这种情况下,必须通过“获取方法”来访问属性,否则,实例变量就永远不会初始化。比如说,EOCPerson 类也许会用一个属性来表示人脑中的信息,这个属性所指代的对象相当复杂。由于此属性不常用,而且创建该属性的成本较高,所以,我们可能会在“获取方法”中对其执行惰性初始化:

1 - (EOCBrain *)brain {
2     if (!_brain) {
3         _brain = [Brain new];
4     }
5     return _brain;
6 }

  若没有调用 “获取方法”就直接访问实例变量,则会看到尚未设置好的 brain,所以说,如果使用了 “惰性初始化”技术,那么必须通过存取方法来访问 brain 属性。

END

posted @ 2017-06-18 07:49  鳄鱼不怕牙医不怕  阅读(301)  评论(0编辑  收藏  举报