首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》3.5 属性

关灯直达底部

之前在编写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的运行结果应该和之前的完全相同。