之前在编写BNRItem时,需要为每一个实例变量声明并实现一对存取方法。下面介绍如何通过属性来简化这个过程。通过属性,也可以为类声明实例变量并实现相应的存取方法,而且更简便。使用属性后,可以大幅减少所需编写的代码量,并且写出的类文件也更容易理解。
声明属性
下面先给出一则属性声明的示例:
@property NSString *itemName;
声明一个属性,等于隐含地为相应名称的实例变量声明一对存取方法。请看表3-1,左边是没有使用属性的类,右边则使用了属性,但是两个类是完全等价的。
表3-1 使用和不使用属性的两个等价类
表3-1中的两个类都具有一个实例变量_name和一对存取方法,左边的类中需要将声明和实现都明确地写出来,而右边只需要声明一个属性就可以达到相同的效果。
下面修改BNRItem类,使用属性替换实例变量和存取方法。
打开BNRItem.h,删除所有实例变量和存取方法声明,改为相应的属性,代码如下:
@interface BNRItem : NSObject
{
NSString *_itemName;
NSString *_serialNumber;
int _valueInDollars;
NSDate *_dateCreated;
BNRItem *_containedItem;
__weak BNRItem *_container;
}
@property BNRItem *containedItem;
@property BNRItem *container;
@property NSString *itemName;
@property NSString *serialNumber;
@property int valueInDollars;
@property NSDate *dateCreated;
+ (instancetype)randomItem;
- (instancetype)initWithItemName:(NSString *)name
valueInDollars:(int)value
serialNumber:(NSString *)sNumber;
- (instancetype)initWithItemName:(NSString *)name;
- (void)setItemName:(NSString *)str;
- (NSString *)itemName;
- (void)setSerialNumber:(NSString *)str;
- (NSString *)serialNumber;
- (void)setValueInDollars:(int)v;
- (int)valueInDollars;
- (NSDate *)dateCreated;
- (void)setContainedItem:(BNRItem *)item;
- (BNRItem *)containedItem;
- (void)setContainer:(BNRItem *)item;
- (BNRItem *)container;
@end
现在的BNRItem.h可读性更好,代码如下:
@interface BNRItem : NSObject
+ (instancetype)randomItem;
- (instancetype)initWithItemName:(NSString *)name
valueInDollars:(int)value
serialNumber:(NSString *)sNumber;
- (instancetype)initWithItemName:(NSString *)name;
@property BNRItem *containedItem;
@property BNRItem *container;
@property NSString *itemName;
@property NSString *serialNumber;
@property int valueInDollars;
@property NSDate *dateCreated;
@end
请注意属性的名字是实例变量的名字去掉下画线,编译器根据属性生成实例变量时会自动在变量名前加上下画线。
如果声明了一个名为itemName的属性,编译器会自动生成实例变量_itemName、取方法itemName和存方法setItemName:。(请注意实例变量和存取方法的声明不会出现在文件中,编译器会在编译时自动加入这些代码)因此程序能像之前一样正常工作。
声明属性还可以为相应的存取方法生成代码。在BNRItem.m中,删除之前实现的存取方法。
- (void)setItemName:(NSString *)str
{
_itemName = str;
}
- (NSString *)itemName
{
return _itemName;
}
- (void)setSerialNumber:(NSString *)str
{
_serialNumber = str;
}
- (NSString *)serialNumber
{
return _serialNumber;
}
- (void)setValueInDollars:(int)p
{
_valueInDollars = p;
}
- (int)valueInDollars
{
return _valueInDollars;
}
- (NSDate *)dateCreated
{
return _dateCreated;
}
- (void)setContainedItem:(BNRItem *)item
{
_containedItem = item;
item.container = self;
}
- (BNRItem *)containedItem
{
return _containedItem;
}
- (void)setContainer:(BNRItem *)item
{
_container = item;
}
- (BNRItem *)container
{
return _container;
}
读者可能会注意到setContainedItem:方法。该方法除了设置_containedItem实例变量外,还设置了传入的BNRItem对象的_container实例变量。之后读者会学习如何自定义存取方法。接下来学习有关属性的基本知识。
属性的特性
任何属性都可以有一组特性(attribute),用于描述相应存取方法的行为。这些特性需要写在小括号里,并跟在@property指令后面,示例如下:
@property (nonatomic, readwrite, strong) NSString *itemName;
任何属性都有三个特性,每个特性都有多种不同的可选类型。在这些可选类型中,有一种是默认的。如果属性的某个特性使用默认类型,就可以在声明该属性时忽略这项特性。
多线程特性
多线程特性(Multi-threading attribute)有两种可选类型:nonatomic和atomic。(多线程超出了本书的讨论范围,这里只需要知道有这两个类型即可。)大多数Objective-C程序员会将这个特性设置为nonatomic,Big Nerd Ranch也是,Apple也是。本书代码中的所有属性都会使用nonatomic。
因为nonatomic不是默认类型,所以在声明属性时,必须明确地写出nonatomic。
打开BNRItem.h,为所有属性添加nonatomic特性,代码如下:
@interface BNRItem : NSObject
+ (instancetype)randomItem;
- (instancetype)initWithItemName:(NSString *)name
valueInDollars:(int)value
serialNumber:(NSString *)sNumber;
- (instancetype)initWithItemName:(NSString *)name;
@property (nonatomic) BNRItem *containedItem;
@property (nonatomic) BNRItem *container;
@property (nonatomic) NSString *itemName;
@property (nonatomic) NSString *serialNumber;
@property (nonatomic) int valueInDollars;
@property (nonatomic) NSDate *dateCreated;
@end
读/写特性
读/写特性(Read/write attribute)也有两种可选类型:readwrite和readonly。编译器会为具有readwrite特性的属性生成存方法和取方法,如果是readonly类型,则只会生成取方法。第二个特性的默认类型是readwrite。BNRItem中的属性除了dateCreated是readonly类型,其他的都是readwrite类型。
在BNRItem.h中,将dateCreated声明为readonly的属性,要求编译器只为相应的实例变量生成取方法,代码如下:
@property (nonatomic,readonly) NSDate *dateCreated;
内存管理特性
内存管理特性(Memory management attribute)有四种可选类型:strong、weak、copy和unsafe_unretained。这些类型决定相应的实例变量将如何引用对象。
对于不指向任何对象的属性(例如int valueInDollars),不需要做内存管理,这时应该选用unsafe_unretained,它表示存取方法会直接为实例变量赋值。Apple引入ARC之前曾经使用assign表示这种类型。
unsafe_unretained中的“unsafe(不安全)”可能会误导读者。该类型的“不安全”是相对于弱引用而言的。与弱引用不同,unsafe_unretained类型的指针指向的对象被销毁时,指针不会自动设置为nil,而是成为空指针,因此不安全。但是当处理非对象属性(non-object)时,是不会出现空指针问题的。
unsafe_unretained是非对象属性的默认值,所以valueInDollars属性不用明确写出该类型。
对于指向Objective-C对象的属性,四种类型都有可能。默认是strong类型,但是通常程序员会明确写出strong。(Apple曾经修改过默认值,未来仍然可能有改动。)
在BNRItem.m中,为属性添加内存管理特性,其中containedItem和dateCreated属性设置为strong,container属性设置为weak:
@property (nonatomic,strong) BNRItem *containedItem;
@property (nonatomic,weak) BNRItem *container;
@property (nonatomic) NSString *itemName;
@property (nonatomic) NSString *serialNumber;
@property (nonatomic) int valueInDollars;
@property (nonatomic, readonly,strong) NSDate *dateCreated;
将container属性设置为weak是为了避免强引用循环,之前的代码演示过这个问题。
现在itemName和serialNumber属性还没有添加内存管理特性。它们是指向NSString对象的属性。通常情况下,当某个属性是指向其他对象的指针,而且该对象的类有可修改的子类(例如NSString/NSMutableString或NSArray/NSMutableArray)时,应该将该属性的内存管理特性设置为copy。
在BNRItem.m中,将itemName和serialNumber属性的内存管理特性设置为copy:
@property (nonatomic, strong) BNRItem *containedItem;
@property (nonatomic, weak) BNRItem *container;
@property (nonatomic,copy) NSString *itemName;
@property (nonatomic,copy) NSString *serialNumber;
@property (nonatomic) int valueInDollars;
@property (nonatomic, readonly, strong) NSDate *dateCreated;
改用copy特性后,itemName属性的存方法可能类似于以下代码:
- (void)setItemName:(NSString *)itemName
{
_itemName = [itemName copy];
}
和前一个版本的setItemName:方法不同,这段代码没有直接将传入的itemName赋给实例变量_itemName,而是先向itemName发送了copy消息。该对象的copy方法会返回一个新的NSString对象(不是NSMutableString对象),并且其拥有的数据会和收到copy消息的那个对象相同。接着,新版本的setItemName:方法会为实例变量_itemName赋值,指向新的NSString对象。
这样做的原因是,如果属性指向的对象的类有可修改的子类,那么该属性可能会指向可修改的子类对象,同时,该对象可能会被其他拥有者修改。因此,最好先复制该对象,然后再将属性指向复制后的对象。
以BNRItem为例,假设有某个初始化后的BNRItem对象,其实例变量_itemName指向一个NSMutableString对象,代码如下:
NSMutableString *mutableString = [[NSMutableString alloc] init];
BNRItem *item = [[BNRItem alloc] initWithItemName:mutableString
valueInDollars:5
serialNumber:@/"4F2W7/"]];
这段代码是有效的,因为凡是可以使用NSString对象的地方,也可以使用NSMutableString对象(NSMutableString是NSString的子类)。真正的问题是程序可能在BNRItem对象不知情的情况下修改mutableString变量所指向的NSMutableString对象。
当读者可以掌控某个应用的全部代码时,自然可以确保mutableString变量所指向的NSMutableString对象不会被意外地修改。但是,当其他程序员也会使用读者编写的类时,就要做最坏的打算,编写具有“防御性”的代码。
对于这段代码来说,需要加入的防御措施是将itemName属性的内存管理特性设置为copy。
至于所有权,copy方法返回的是拥有强引用特性的指针,而收到copy消息的NSString对象不会发生任何变化:该对象不会获得也不会失去拥有者,其数据也不会发生任何变化。
只有可变对象应该设置为copy,而复制不可变对象会浪费内存空间——不可变对象不会发生上述问题,因为任何对象都无法修改它们。为了避免不必要的复制,向不可变对象发送copy消息时,会返回一个指向自己(仍然是不可变的)的指针。
自定义属性的存取方法
默认情况下,属性自动添加的存取方法非常简单,类似以下代码:
- (void)setContainedItem:(BNRItem *)item
{
_containedItem = item;
}
- (BNRItem *)containedItem
{
return _containedItem;
}
属性自动添加的存取方法大部分是可以直接使用的。但是对于containedItem属性,默认的存方法无法满足要求,setContainedItem:方法需要完成额外的工作:设置传入对象的container属性。
可以在实现文件中编写自定义的存方法,覆盖默认的方法。在BNRItem.m中,加回之前实现的setContainedItem:方法:
- (void)setContainedItem:(BNRItem *)containedItem
{
_containedItem = containedItem;
self.containedItem.container = self;
}
编译器看到自定义的setContainedItem:方法之后,不会再为containedItem属性创建默认的存方法。但是仍然会创建默认的取方法containedItem。
请注意,如果既覆盖了存方法,也覆盖了取方法(或者为只读属性覆盖了取方法),那么编译器就不会再自动创建相应的实例变量了。如果需要实例变量,就必须明确声明。
如果默认的存取方法无法满足要求,必须手动为相应的实例变量实现自定义的存取方法。
现在构建并运行应用,BNRItem的运行结果应该和之前的完全相同。