第6条:理解“属性”这一概念(上)

  本条要点:(作者总结)

  • 可以用 @property 语法来定义对象中所封装的数据。
  • 通过“特质”来指定存储数据所需要的正确语义。
  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
  • 开发 iOS 程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。

  第2章

  对象、消息、运行期

  用 Objective-C 等面向对象语言编程时,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messageing)。若想编写出高效且易维护的代码,就一定要熟悉这两个特性的工作原理。

  当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C 运行期环境”(Objective-C runtime),它提供了一些使用对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。在理解了运行期环境中各个部分协同工作的原理之后,你的开发水平将会进一步提升。

  

  “属性”(property)是 Objective-C 的一项特性,用于封装对象中的数据。Objective-C 对象通常会把其所需的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为 Objective-C 2.0 的一部分,开发者可以令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”(dotsyntax),使开发者可以更为容易地依照类对象来访问存放与其中的数据。你也许已经使用过“属性”这个概念了,不过你未必知道其全部细节。而且,还有很多与属性相关的麻烦事。

  在描述个人信息的类中,也许会存放人名、生日、地址等内容。可以在类接口的 public 区段中声明一些实例变量:

1 @interface EOCPerson : NSObject {
2     @public
3     NSString *_firstName;
4     NSString *_lastName;
5     @private
6     NSString *_someInternalData;
7 }
8 @end

  原来编写过 java 或 C++ 程序的人应该比较熟悉这种写法,在这些语言中,可以定义实例变量的作用域。然而编写  Ojbective-C 代码时却很少这么写做。这种写法的问题是:对象布局在编译期(compile time)就已经固定了。只要碰到访问 _firstName 变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬解码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。这样做目前来看没问题,但是如果又加了一个实例变量,那就麻烦了。比如说,假设在 _firstName 之前又多了一个实例变量:

1 @interface EOCPerson : NSObject {
2     @public
3     NSDate *_dateOfBirth;
4     NSString *_firstName;
5     NSString *_lastName;
6     @private
7     NSString *_someInternalData;
8 }
9 @end

  原来表示 _firstName 的偏移量现在却指向_dateOfBirth 了。把偏移量硬解码于其中的那些代码都会读取到错误的值。请对比在类中加入 _dateOfBirth 这一实例变量之前与之后的内存布局,其中假设指针为 4 个字节。

  Person
+0 _firstName
+4 _lastName
+8 _someInternalData
  Person
+0 _dateOfBirth
+4 _firstName
+8 _lastName
+12 _someInternalData

  在类中新增另一个实例变量前后的数据布局图

  如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如,某个代码库中的代码使用一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象(incompatibility)。各种编程语言都有应对此问题的方法。Objective-C 的做法是,把实例变量当做一种存储偏移量所用的“特殊变量”(special variable),交由“类对象”(class object)保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量,这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。ABI 定义了许多内容,其中一项就是生成代码时所应遵循的规范。有了这种“稳固的”(nonfragile)的ABI,我们就可以在 “class-continuation 分类” 或实现文件中定义实例变量了。所以说,不一定要在接口中把全部实例变量都声明好,可以将某些变量从接口的 public 区段移走,以便保护与类实现有关的内部信息。

  这个问题还有一种解决办法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。虽说属性最终还是得通过实例变量来实现,但它却提供了一种简洁的抽象机制。你可以自己编写存取方法,然而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。正因为有了这种严格的命名规范,所以 Objective-C 这门语言才能根据名称自动创建出存取方法。。这时 @property 语法就派上用场了。

  在对象接口的定义中,可以使用属性,这是一种标准的写法,能够访问封装在对象里的数据。因此,也可以把属性当做一种简称,其意思是说:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。例如下面这个类:

1 @interface EOCPerson : NSObject
2 @property NSString *firstName;
3 @property NSString *lastName;
4 @end

  对于该类的使用者来说,上述代码写出来的类与下面之中写法等效:

1 @interface EOCPerson : NSObject
2 - (NSString *)firstName;
3 - (void)setFirstName:(NSString *)firstName;
4 - (NSString *)lastName;
5 - (void)setLastName:(NSString *)lastName;
6 @end

  要访问属性,可以使用“点语法”,在纯 C 中,如果想访问分配在栈上的 struct 结构体里面的成员,也需要使用类似语法。编译器会把“点语法”转换为存取方法的调用,使用“点语法”的效果与直接调用存取方法相同。因此使用“点语法”和直接调用存取方法之间没有丝毫差别。通过下列范式代码可以看出,这两者等效:

1     EOCPerson *aPerson = [Person new];
2     aPerson.firstName = @"Bob"; // same as:
3     [aPerson setFirstName:@"Bob"];
4     
5     NSString *lastName = aPerson.lastName; // Same as:
6     NSString *lastName = [aPerson lastName];

  然而属性还有更多优势。如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里面看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

1 @implementation EOCPerson
2 
3 @synthesize firstName = _myFirstName;
4 @synthesize lastName = _myLastName;
5 
6 @end

  前述语法会将生成的实例变量命名为  _myFirstName 与 _myLastName,而不再使用默认的名字。一般情况下无须修改默认的实例变量的名字,但是如果你不喜欢以下划线来命名实例变量,那么可以使用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看的懂。

  若不想令编译器自动合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法,那么另外一个还是会由编译器来合成。还有一种办法能阻止编译器自动合成存取方法,就是使用 @dynamic 关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。比方说,如果从 CoreData 框架中的 NSManagedObject 类里继承了一个子类,那么就需要在运行期动态创建存取方法。继承 NSManagedObject 时之所以这样做,是因为子类的某些属性不是实例变量,其数据来自后端的数据库中。例如:

 1 // .h
 2 @interface EOCPerson : NSManagedObject
 3 
 4 @property NSString *firstName;
 5 @property NSString *lastName;
 6 
 7 @end
 8 
 9 // .m
10 @implementation EOCPerson
11 
12 @dynamic firstName, lastName;
13 
14 @end

  编译器不会为上面这个类自动合成存取方法或实例变量。如果用代码访问其中的属性,编译器也不会发出警示信息。

 END

posted @ 2017-06-16 01:28  鳄鱼不怕牙医不怕  阅读(222)  评论(0编辑  收藏  举报