2.4 属性(Property)

回顾前面的代码,大家也许会想,为什么对类的成员变量需要通过set***之类的方法来访问?为什么不直接设置成员变量值呢?这是因为在头文件中定义的成员变量默认访问权限设置的是@protected,也就是只有类自身及子类能够访问,为此要添加设置器方法来允许从外部修改在头文件中定义的成员变量。如果需要从类外部直接读取类头文件中定义的成员变量,还要添加相应的读取器方法。

有没有什么方法可以让这个步骤变得更简单吗?答案是有的,这就是属性的作用。属性的关键字是@property,对于标记为属性的成员变量,Xcode 4.5及以后的版本会自动生成相应的读取设置器。但对于使用Xcode 4.5以前版本的开发者来说,仅仅标记@property是不够的,必须通过另外一个命令@synthesize来告诉编译器自动生成读取设置器。相信大家在读代码尤其是老一些的代码时会看到很多这样的关键字,为此我们介绍完@property后,会介绍@synthesize的相关使用方法。

2.4.1 设置属性

属性关键字@property 用来在头文件中对成员变量进行标注,为成员变量声明属性,编译器看到此标记后会自动帮助成员变量设置读取设置器接口。

2.4.2 以Person类为例

回顾一下Person类头文件Person.h的源代码:

        1: #import <Foundation/Foundation.h>
        2: @interface Person : NSObject
        3: {
        4:   int age;
        5:   char gender;
        6:}
        7:
        8: -(void)setAge: (int)theAge;
        9: -(void)setGender: (char)theGender;
        10: -(void)print;
        11:
        12:@end

在这段代码中定义了两个成员变量 age 和 gender,并分别有一个设置器方法 setAge:和setGender:。如果使用属性,将可以省掉这两个方法:

        1: #import <Foundation/Foundation.h>
        2: @interface Person : NSObject
        3: {
        4:   int age;
        5:   char gender;
        6:}
        7:
        8:  @property int age;
        9:  @property char gender;
        10: -(void)print;
        11:
        12:@end

在第8、9行的两个方法声明的位置上,我们改为用@property来标记两个成员变量,看似挺合理的语法,但总会感觉为什么第4、5行声明了一次,在第8、9行为了标记属性又要声明一次?不用诧异,这样的代码语法是没有问题的,有了属性,开发者将能够通过类似self.age的方法来访问成员变量age,大家也会在很多代码或者书里面看到重复声明这样的代码写法。

但这其实是很久以前版本的Xcode上的写法了,通过@property进行标记的成员变量已经无须事先声明一次。上面的代码可以简化如下:

        1: #import <Foundation/Foundation.h>
        2: @interface Person: NSObject
        3:
        4: @property int age;
        5: @property char gender;
        6: -(void)print;
        7:
        8:@end

2.4.3 @synthesize指令

如果是在Xcode 4.5及以后版本的开发环境中进行开发,当编译时编译器会自动帮助添加取设置器,为了演示在 Xcode 4.5 以前使用@synthesize 指令的方法,我们手动添加@synthesize指令。

为此,可以修改Person.m类文件进行如下修改:

        1: #import "Person.h"
        2: @implementation Person
        3: @synthesize age=_age;
        4: @synthesize gender=_gender;
        5:
        6: -(void)print
        7:{
        8:   NSLog(@"年龄:%d;性别:%@", self.age, self.gender=='M'?@"男性":@"女性");
        9: }
        10: @end

注意上面的代码中,在第3、4行添加了@synthesize指令,第3行的指令告诉编译器为属性age创建读取设置器,并指定了一个内部同名变量_age。同样第4行指令告诉编译器为 gender 创建读取设置器,并指定了一个内部同名变量_gender。需要留意的是如果阅读代码时看到了如下的代码页,不必觉得奇怪:

        1: @synthesize age;

这种写法实际上是以下代码的简写:

        1: @synthesize age=age;

相当于这个成员变量的内部同名变量也是 age,这样容易引起理解上的误会,建议还是像前面那样,用下画线来进行区分。

这样_age和_gender是两个可以在内部直接使用的变量名,而age和gender属性名则可以供类的内外部通过点表达式来调用。例如在外部可以按照如下方法调用:

        1: Person *person = [[Person alloc] init];
        2: person.age = 18;
        3: person.gender = 'M';

上面代码的第2、3行通过点表达式的方式来直接设置成员变量的值,在没加@property标记之前这都是不可以的,如果强行这么做会收到编译器警告。而且通过这种方式设置age和gender属性时,会通过Xcode自动生成的设置器方法来设置,而不是直接访问成员变量本身。如何验证这一点?可以改写设置器来覆盖自动生成的设置器,在前面Person.m代码中添加如下方法:

        1: -(void)setAge: (int)theAge
        2: {
        3:   NSLog(@"设置器测试");
        4:   _age = theAge;
        5: }

实际上又将前面的setAge:方法的代码加了回来,不过在第3行增加了一条调试信息。接着在main.m文件中添加如下代码来进行测试:

        1: int main(int argc, const char *argv[])
        2: {
        3:   Person *person = [[Person alloc] init];
        4:   person.age = 18;
        5: }

运行代码,忽略编译器的警告信息(这实际上跟多线程访问变量时的原子性相关,这里无须深究),可以看到如图2-24所示,setAge:方法中的日志被输出了。

图2-24 setAge:方法日志输出

这是因为@synthesize 要求编译器自动添加的设置器方法名就是 setAge:,这个方法名是从变量名过来的,因此当我们重写了setAge:方法时,Xcode编译器会用我们重写的方法替代掉自动生成的设置器。

在内部调用成员变量时,可以结合self来进行调用,例如:

        1: NSLog(@"年龄:%d;性别:%@", self.age, self.gender=='M'?@"男性":@"女性");

这里访问成员变量的方法实际上有两种,例如一种是通过self.age,另外一种是通过内部的变量名_age,但推荐使用 self.age 的方式进行访问,原因是通过这种方式进行访问时会调用读取设置器来进行访问。在读取设置器中有一些代码逻辑比如值校验的情况下通过self.age 这样的方式可以保证访问的统一性,而通过_age 这样的方式将不经过读取设置器而直接访问变量本身。

还需要说明的是,直接通过age这样的方式是访问不了的,因为现在Person类在内部变量中并不存在这样的变量名,内部的变量名是_age而不是age。

2.4.4 Xcode 4.5以后版本对@synthesize指令的处理

如果是在Xcode 4.5及之后的版本中开发代码,可以直接将Person.m类文件中下面这两行代码删掉:

        1: @synthesize age = _age;
        2: @synthesize gender = _gender;

这是因为Xcode会自动帮助属性添加读取设置器,相当于自动添加了上面两行代码,因此没必要再单独添加了。

2.4.5 属性特性

属性的好处还在于我们可以为属性指定特性来更加丰富变量的定义。带特性的属性标记格式如下:

        @property(attribute1, attribute2…) type name;

这与我们之前看到的属性标记的区别在于@property 后面添加了一堆括号,且在括号内可以指定一些额外的标记,这些额外的标记就称为“特性”,特性的个数可以是0,也可以是多个,有多个特性标记的情况下使用逗号来区分每一个特性标记。本节中介绍一些最常用到的特性标记。

1.原子性特性

原子性的概念在于在多线程访问的情况下如何处理数据同步的问题,关于多线程开发,在后续章中节会有更详细的介绍。简单来说如果两个用户同时在修改同一个变量,那么应该怎么保证变量值的统一性?原子的定义是化学反应中最小的不可分的单位,同理,原子性的概念是指保证变量在一个时间点上只有一个线程在访问。

属性默认的特性是原子性的,用atomic进行标记,所以前面对于age、gender的属性标记相当于是:

        1: @property(atomic) int age;
        2: @property(atomic) char gender;

但iOS应用开发大多数情况下并不用考虑多线程的问题,在一台iOS设备中,一个应用只能启动一个实例,也只有一个用户在使用,如果一定采用默认的原子性标记,会导致iOS系统对原子性处理的额外的性能开销。因此在大多数情况下,没有必要保留默认的特性,标记为原子性,为此可以显式地使用nonatomic特性来进行标记。例如对于上面的两个变量可以进行如下标记:

        1: @property(nonatomic) int age;
        2: @property(nonatomic) char gender;

2.可读写性特性

可读写性的概念是可以通过特性来标记属性是否可读、可写。有如下两个值可以用来进行标记。

readwrite:这个特性标记是默认的值,标记属性既可以读,也可以写,既可以读取属性的值,也可以修改属性的值;

readonly:这个特性标记表明属性只可读,不可写,虽然可以读取属性的值,但是不可以修改属性的值。

例如,可以将age属性标记成readonly,可留意到我们也保留了nonatomic属性标记:

        1: @property(nonatomic, readonly) int age;

标记为readonly后,如果还想通过如下代码在main.m中设置age属性值,将会遇到编译错误,如下main.m中的代码将不会被编译通过:

        1: Person *person = [[Person alloc] init];
        2: person.age = 20;

编译时将会遇到编译器错误:“Assignment to readonly property”。

3.访问方法名特性

默认的属性读取器的名字和属性名是一样的,而设置器的名字的命名规则是setPropertyName:,例如,属性的名字是 foo,那么读取器的名字也是 foo,设置器的名字就是setFoo:。如果需要为读取设置器指定特别的名字,可以使用方法名特性。有如下两个值可以用来进行标记。

getter=getterName:等号后面的参数指定了读取器的方法名;

setter=setterName:等号后面的参数指定了设置器的方法名。

例如通过下面的代码来为age属性指定不同的设置器名字,注意因为设置器有参数输入,在方法名后一定要加上冒号:

        1: @property(nonatomic, setter = ageSetter: )int age;

现在可以在代码中通过如下方式来为age属性赋值了:

        1: Person *person = [[Person alloc] init];
        2: person.age = 20;
        3: [person ageSetter:26];

上面代码的第3行使用新定义的设置器方法名来为age属性赋值,可以留意到,我们仍然可以通过.age的方式来修改属性值,但是如果通过以下方法设置属性值,将会收到编译器错误提示:

          1: [person setAge:26];//这样的代码无法编译通过

通常情况下都不会用到访问方法名特性,但是在有些情况下,比如我们有一个布尔类型(BOOL)的值,按照很多开发者的习惯,会使用 isPropertyName 或者 hasProperName这样的方式来访问属性。

留意同时使用setter=特性标记和readonly特性标记会产生冲突,如下的代码会收到编译器警告信息:

          1: @property(nonatomic, readonly, setter = ageSetter: ) int age;

4.与内存管理相关的特性

尽管从Xcode 4.3开始,Xcode已经很少需要考虑内存管理即内存的使用、回收和释放等,从Xcode 4.5开始几乎不用考虑内存管理,但仍然有如下两个与内存管理相关的特性需要予以说明。

strong:strong标记说明当前的类对属性对象是强持有的关系,当类本身的内存被释放的时候,该属性需要进行清零即设置其值为nil。

weak:weak标记说明当前的类对属性对象是弱持有的关系,当类的内存被释放的时候,该属性的值将被自动清零。

注意由于是内存管理相关,因此能够标记相关特性的属性一定要是对象类型,不能是基本数据类型。下面的代码将Student类的schoolName属性设置为强持有:

          1: @property(nonatomic, strong) NSString *schoolName;

该标记为强持有还是弱持有在为iOS应用的视图添加属性代码时会遇到,届时再详细介绍。另外如copy、assign、retain等标记在Xcode 4.2以后已经很少用到,读者如果感兴趣,可以翻阅苹果公司开发者资源库了解更多详情。