属性是什么

使用属性编程

一般来说,属性指的是一个对象的属性或特性。
本文中要说明的属性声明是一种声明变量为属性的语法,该语法同时还引入了一种更简单的访问属性的方法。至今为止的说明中,对象的属性值都是保存在一个实例便来那个中,然后直接访问或者通过访问方法访问。也就是说,声明了实例变量或定义了访问方法就相当于实现了属性。本文中要介绍的属性声明指的就是在接口文件中声明实例对象到底有哪些属性。

因为实现方法写在类的实现部分中,所以对外部是不可见的。从功能的角度来看,虽然和使用访问方法并没有不同,但可以把访问方法的声明从类的接口文件中删除。
属性声明的一些规则有:

  • 自动生成访问方法:能够为指定的实例变量自动生成访问方法。既可以同时生成getter和setter方法,也可以只生成getter方法。也可以手动定义访问方法。
  • 自动生成实例变量:如果不存在同名的实例变量的话,在生成访问方法的同时,也会自动生成同名的实例变量。
  • 更简单地调用访问方法:可以通过点操作符(.)来调用访问方法。无论是赋值用的setter方法还是返回值用的getter方法,都可以通过点操作符调用。只要定义了访问方法,就可以使用点操作符来调用。
  • 属性的内省: 通过内省可以动态查询类中声明的属性一级属性的名称和类型。

属性的概念

属性这个词,在不同的上下文中有不同的含义。在以前的Objective-C中,属性并没有被用来表示特别的功能,但从KVC(Key-Value Coding)开始,属性就被赋予了“从外部可以访问的对象的属性“这层含义。

属性的声明和功能

显式声明属性

通过@property编译器指令,后跟属性的类型信息和名称,就可以显式声明属性

1
@property int hitPoint;

属性声明等同于声明了读写两个访问方法。
属性声明的时候还可以为属性自定义选项。选项位于圆括号中,前面是@property指令。例如

1
@property(readonly) NSString *name;

就声明了一个只读属性。

给属性指定选项

可以同时给一个变量指定多个选项,选项之间需要用逗号分隔

种类 选项 说明
指定方法名 getter=getter方法名 setter=setter方法名 显式指定getter方法和setter方法的名字
读写属性 readonly/readwrite 只读/读写
赋值时的选项 assign 单纯赋值
赋值时的选项 retain 进行保持操作
赋值时的选项 unsafe_unretained 同assign,用于ARC
赋值时的选项 strong 同retain一样,用于ARC
赋值时的选项 weak 同assign,用于ARC
赋值时的选项 copy 复制对象
原子性操作 nonatomic 非原子性操作,非线程安全

赋值时的选项

我们可以为可读写的@property设置选项,选项一共有上述的六种。选项之间是排他的关系,可以不设置任何选项或只设置6种中的一种。根据所修饰的属性是否是对象类型或者所采用的内存管理的不同,选项的意义也会发生变化。

当属性是对象类型,且使用ARC管理内存时。如果不指定任何选项,编译时会提示警告。如果指定了assign或者unsafe_unretained选项的情况下,只是进行单纯的赋值,不进行保持操作。声明属性对应的实例变量时需要加上_-unsafe_unretained修饰符。因为没有被保持,所以实例变量指向的内容可能会被释放掉而变成野指针。指定了strong或者retain选项时,赋值操作之后还会对传入的变量进行保持操作。实例变量声明时需要不加任何修饰符或使用__strong修饰符。指定weak选项的情况下,会生成相当于弱引用赋值的代码。实例变量在声明时需要加上__weak修饰符。
指定了copy选项的情况下,会使用copy方法建立传入值的一份副本,并使用这份副本给实例变量进行赋值。

原子性

nonatomic选项是在多线程环境下使用的。nonatomic表示访问方法是非原子的。
原子性是多线程中的一个概念,如果说访问方法是原子的,那就意味着多线程环境下访问属性是安全的,在执行的过程中不可被打断。而nonatomic则正好相反,表明访问是可以被打断的。缺省的情况下,访问方法默认是原子的。
在没有指定nonatomic的时候,访问方法需要使用lock和unlock来保证方法的原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (TYPE)name {
[_ex lock]; //局部锁
TYPE rtn = [[name retain] autorelease];
[_ex unlock];
return rtn;
}

-(void)setName: (TYPE)obj {
[_ex lock];
if (name != obj) {
[name release];
name = [obj retain];
}
[_ex unlock];
}

而如果指定了nonatomic,则在多个线程中同时调用getter或setter,就有可能出现值丢失或内存泄漏等错误。通过使用lock和unlock,能够确保每次最多只有一个线程在执行lock和unlock之间的代码,从而保证了原子性。上述例子中的getter方法除了追加了lock和unlock还增加了retain和autorelease的操作,用来防止在返回实例变量时,另外一个线程释放了这个实例变量。

通过点操作符访问属性

点操作符的使用方法

Objective2.0会在编译时把使用点操作符访问属性的过程理解为访问方法的调用。因为调用的是访问方法,所以无论对应的实例变量是否存在,只要访问方法存在,就都可以通过点操作符访问属性。
点操作符只能用于类类型的变量,不能对id类型的变量应用点操作符。因为没指定具体类型的情况下,编译器无法判断是否存在属性对应的访问方法。除了id类型外,void类型和C语言中的数组类型的变量也都不能使用点操作符。

复杂的点操作符使用

连用点操作符:
点操作符可以连用:

1
2
n = obj.productList.length;
obj.contents.enabled = YES;

因为点操作符按照从左向右的顺序进行解析,所以上面的表达式可被解析为

1
2
n = [[obj productList] length];
[[obj contents] setEnabled: YES];

当一个对象的实例变量是另外一个对象时,可通过连用点操作符来访问对象的实例变量中的成员。如果连用表达式中有一个nil,则整个表达式的返回值就是nil。
连续赋值:

1
2
n = 0;
k = obj.count = obj.depth = ++n;

赋值的解析是从右向左的,因此上述代码可以理解为

1
k = (obj.count = (obj.depth = ++n));

最终k、obj.count和obj.depth都为1;
对递增、递减和复合运算符的解释:

1
e = obj.depth++;

会先进行赋值然后进行自增,相当于

1
2
e = [obj depth];
[obj setDetph: [obj depth] + 1];

复合表达式也相同
self使用点操作符:
类的方法中可以i通过对self应用点操作符来调用自己的访问方法。但需要注意的是,不要再访问方法中使用self来访问对应的属性,否则会造成无限循环的调用。

1
2
self.count = 12;
obj.depth = self.depth + 1;

super使用点操作符:
通过super使用点操作符可以访问父类中定义的setter和getter方法
和结构体的成员混用:
获取类属性的点操作符和访问结构体元素的点操作符可以混用,但修改类属性的点操作符和访问结构体元素的点操作符不可混用。比如说当属性是个结构体变量时

1
w = win.minSize.width;

其中minSize属性就是一个结构体类型,这样的访问是没有问题的,等价于

1
w = [win minSize].width;

但是不能使用这种直观的写法来为width赋值

1
win.minSize.width = 320; // 不允许

想要为minSize的width赋值只能这样写

1
2
3
NSSize sz = win.minSize;
sz.width = 32;
win.minSize = sz;

只能允许使用NSSize类型的结构体变量对minSize进行赋值
虽然可以像&cell.size这样通过取地址符号来对结构体的成员取地址,但不能通过取地址符号来对点操作符获得的属性取地址,就像不能对函数的返回值直接取地址一样。
还有,当想给obj的实例变量contents发消息时,你可能会这样写

1
[obj.contents retain];

但需要注意的是,实际上这行语句表示的是给getter方法的返回值发消息,并不一定会给obj的实例变量contents发送消息。
访问对象实例变量的最正统的方式还是通过->操作符。