首页 > ios > 第6条:理解“属性”这一概念(1)

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

第2章 对象、消息、运行期

  用Objective-C等面向对象语言编程时,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。若想编写出高效且易维护的代码,就一定要熟悉这两个特性的工作原理。
  当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。在理解了运行期环境中各个部分协同工作的原理之后,你的开发水平将会进一步提升。

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

  “属性”(property)是Objecive-C的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为Objective-C 2.0的一部分,开发者可以令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”(dot syntax),使开发者可以更为容易地依照类对象来访问存放于其中的数据。你也许已经使用过“属性”这个概念了,不过你未必知道其全部细节。而且,还有很多与属性有关的麻烦事。第6条将会告诉大家有哪些问题可以用属性来解决,并指出其中所体现出来的关键特性。
  在描述个人信息的类中,也许会存放人名、生日、地址等内容。可以在类接口的public区段中声明一些实例变量:

@interface EOCPerson : NSObject {  
@public  
    NSString *_firstName;  
    NSString *_lastName;  
@private  
    NSString *_someInternalData;  
}  
@end 

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

@interface EOCPerson : NSObject {  
@public  
    NSDate *_dateOfBirth;  
    NSString *_firstName;  
    NSString *_lastName;  
@private  
    NSString *_someInternalData;  
}  
@end 

  原来表示_firstName的偏移量现在却指向_dateOfBirth了。把偏移量硬编码于其中的那些代码都会读取到错误的值。图2-1演示了此情况,请对比在类中加入_dateOfBirth这一实例变量之前与之后的内存布局,其中假设指针为4个字节。
数据布局图
  如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如,某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象(incompatibility)。各种编程语言都有应对此问题的办法。Objective-C的做法是,把实例变量当做一种存储偏移量所用的“特殊变量”(special variable),交由“类对象”(class object)保管(第14条详述了类对象)。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。甚至可以在运行期向类中新增实例变量,这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。ABI定义了许多内容,其中一项就是生成代码时所应遵循的规范。有了这种“稳固的”(nonfragile)的ABI,我们就可以在“class-continuation分类”或实现文件中定义实例变量了。所以说,不一定要在接口中把全部实例变量都声明好,可以将某些变量从接口的public区段里移走,以便保护与类实现有关的内部信息。
  这个问题还有一种解决办法,就是尽量不要直接访问实例变量,而应该通过存取方法来做。虽说属性最终还是得通过实例变量来实现,但它却提供了一种简洁的抽象机制。你可以自己编写存取方法,然而在正规的Objective-C编码风格中,存取方法有着严格的命名规范。正因为有了这种严格的命名规范,所以Objective-C这门语言才能根据名称自动创建出存取方法。这时@property语法就派上用场了。
  在对象接口的定义中,可以使用属性,这是一种标准的写法,能够访问封装在对象里的数据。因此,也可以把属性当做一种简称,其意思是说:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。例如下面这个类:

@interface EOCPerson : NSObject  
@property  NSString *firstName;  
@property NSString *lastName;  
@end 

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

@interface EOCPerson : NSObject  
- (NSString*)firstName;  
- (void)setFirstName:(NSString*)firstName;  
- (NSString*)lastName;  
- (void)setLastName:(NSString*)lastName;  
@end 

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

EOCPerson *aPerson = [Person new];  
 
aPerson.firstName = @"Bob"; // Same as:  
[aPerson setFirstName:@"Bob"];  
 
NSString *lastName = aPerson.lastName; // Same as:  
NSString *lastName = [aPerson lastName]; 

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

@implementation EOCPerson  
@synthesize firstName = _myFirstName;  
@synthesize lastName = _myLastName;  
@end 
  1. 还没有评论
评论提交中, 请稍候...

留言


可以使用的标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Trackbacks & Pingbacks ( 0 )
  1. 还没有 trackbacks