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

《iOS编程(第4版)》2.4 创建Objective-C类的子类

关灯直达底部

本节要创建一个名为BNRItem的NSObject子类(见图2-9)。

图2-9  BNRItem类的层级结构

BNRItem对象表示某人在真实世界拥有的一件物品,例如笔记本电脑、自行车、背包等。在模型-视图-控制器设计模式中,BNRItem属于模型类,BNRItem对象用来存储私人物品信息。

创建BNRItem类之后,将使用BNRItem代替NSString,在items数组中存储BNRItem对象(见图2-10)。

图2-10  使用BNRItem代替NSString

在本书后续章节中,读者会在开发一个复杂的iOS应用时重用BNRItem。

创建NSObject子类

用Xcode创建新类的方法为:选择菜单File→New→File…,出现新的面板后,选择面板左侧OS X部分下的Cocoa,然后选择面板右侧的Objective-C class并单击Next按钮(见图2-11)。

图2-11 创建新类

在新出现的面板中,将新类命名为BNRItem(标题为Class的文本框),并将其父类设置为NSObject(标题为Subclass of的文本框),然后单击Next按钮(见图2-12)。

图2-12 选择父类

Xcode会显示一个下拉窗口,提示读者为类文件选择保存位置,全部使用默认设置就可以了。确保选中了Targets中RandomItems前的选择框。单击Create按钮。

在项目导航面板中,找到BNRItem类文件——BNRItem.h和BNRItem.m:

•BNRItem.h是头文件(header file),也称为接口文件(interface file),负责声明类的类名、类的父类、每个类的对象都会拥有的实例变量及该类实现的全部方法。

•BNRItem.m是实现文件(implementation file),包含BNRItem类所实现的方法的全部代码。

每个Objective-C类都有两个这样的文件。读者可以将头文件看成某个类的“用户手册”,将实现文件看成“工程细节”,后者决定类实际会怎样工作。

在项目导航面板中选择BNRItem.h,Xcode会在编辑器区域显示该文件。BNRItem.h目前的代码如下:

#import <Foundation/Foundation.h>

@interface BNRItem : NSObject

@end

要在Objective-C中声明类,需要使用@interface指令,后跟类名,接着为冒号,冒号后面为父类的类名。Objective-C只允许单继承,所有的类都只能有一个父类:

@interface ClassName : SuperclassName

以上这段代码中的@end指令代表BNRItem类的声明至此结束。

请注意前缀@。Objective-C保留了C语言的关键字,并增加了若干Objective-C特有的关键字,新增加的关键字都用前缀@加以区分。

实例变量

真实世界中的物品会有名称、序列号、价值和创建日期。可以将它们设置为BNRItem的实例变量。

声明类的实例变量时,需要将相应的声明写在花括号里,并紧跟在类声明的后面。在BNRItem.h中,为BNRItem类添加一对花括号和四个实例变量:

#import <Foundation/Foundation.h>

@interface BNRItem : NSObject

{

NSString *_itemName;

NSString *_serialNumber;

int _valueInDollars;

NSDate *_dateCreated;

}

@end

这样,每个BNRItem对象都会有四个空位(spot),其中一个用于存放int整数,另外三个用于存放指向对象的指针,分别指向两个NSString对象和一个NSDate对象(请读者记住,*代表相应的变量是指针)。图2-13是一个BNRItem对象的例子,其实例变量都已经赋值。

图2-13一共显示了四个对象:一个BNRItem对象、两个NSString对象和一个NSDate对象。这里的每个对象都是独立的,和其他对象没有关联。BNRItem对象的三个实例变量是指向三个对象的指针,并没有直接保存这些对象。

图2-13  一个BNRItem对象

例如,每个BNRItem对象都有一个名为_itemName的实例变量(指针类型)。图2-13中的BNRItem对象,其_itemName指向一个NSString对象,该对象的内容是Red Sofa(红色沙发)。但是这个BNRItem对象并不是保存Red Sofa字符串,而是将该字符串在内存中的地址赋给_itemName。读者可以将这种关系视为BNRItem对象将Red Sofa字符串命名为_itemName。

实例变量_valueInDollars的情况则不同。它不是指向其他对象的指针,而只是一个int类型的变量。对象会直接保存非指针类型的实例变量。指针的概念不容易理解,第3章会详细介绍对象、指针和实例变量。此外,还会使用对象图(见图2-13)来阐明对象和“指向对象的指针”之间的差别。

存取实例变量

为BNRItem对象添加实例变量后,还要能够存取这些变量的方法。在面向对象的编程语言中,这类存取实例变量的方法称为存取方法(accessor method),即存方法和取方法。如果没有存取方法,就无法访问对象的实例变量。

在BNRItem.h中,为BNRItem对象的实例变量声明存取方法。实例变量_valueInDollars、_itemName和_serialNumber需要存方法和取方法,而实例变量_dateCreated是只读的(read-only),不能修改,因此只需要取方法。

#import <Foundation/Foundation.h>

@interface BNRItem : NSObject

{

NSString *_itemName;

NSString *_serialNumber;

int _valueInDollars;

NSDate *_dateCreated;

}

- (void)setItemName:(NSString *)str;

- (NSString *)itemName;

- (void)setSerialNumber:(NSString *)str;

- (NSString *)serialNumber;

- (void)setValueInDollars:(int)v;

- (int)valueInDollars;

- (NSDate *)dateCreated;

@end

在Objective-C中,存方法的命名规则为英文单词set加上要修改的实例变量的变量名(首字母大写)。以itemName为例,其存方法的方法名是setItemName:。在其他语言中,取方法的方法名通常会是getItemName。但在Objective-C中,取方法的方法名就是实例变量的变量名。Cocoa Touch库中的部分代码会假定读者所编写的类也遵守这样的约定。因此,有着良好代码风格的Cocoa Touch程序员都会遵守这个约定。

(有Objective-C经验的读者请注意,第3章会介绍属性(@property)。)

接下来打开BNRItem的实现文件,BNRItem.m。

任何一个类的实现文件,都必须在其顶部导入自己的头文件。类的实现需要知道相应的类是如何声明的。导入(#import)和C语言中的包含(#include)作用相同,差别是#import可以确保不会重复导入同一个文件。

位于导入语句下面的是实现程序段(implementation block)。实现程序段从@implementation指令开始,后跟要实现的类的类名。实现文件中的所有方法定义都要写在实现程序段里。实现程序段以@end指令结束。

在BNRItem.m中,删除@implementation和@end之间由项目模板加入的代码,然后为BNRItem.h中声明的实例变量实现存取方法。

#import "BNRItem.h"

@implementation BNRItem

- (void)setItemName:(NSString *)str

{

_itemName = str;

}

- (NSString *)itemName

{

return _itemName;

}

- (void)setSerialNumber:(NSString *)str

{

_serialNumber = str;

}

- (NSString *)serialNumber

{

return _serialNumber;

}

- (void)setValueInDollars:(int)v

{

_valueInDollars = v;

}

- (int)valueInDollars

{

return _valueInDollars;

}

- (NSDate *)dateCreated

{

return _dateCreated;

}

@end

在以上这段代码中,存方法将传入的参数赋给了实例变量;取方法则返回实例变量的值。

现在,如果Xcode提示有错误,请读者检查代码并修复(大小写错误或者漏掉分号等)。

下面开始测试新创建的类和存取方法。首先在main.m中导入BNRItem的头文件。

#import <Foundation/Foundation.h>

#import "BNRItem.h"

int main (int argc, const char * argv)

{

...

读者可能会问为什么导入BNRItem.h而不导入NSMutableArray.h?这是因为NSMutableArray包含在Foundation框架中,只要导入Foundation/Foundation.h,就会同时导入NSMutableArray。BNRItem则位于仅包含自己一个类的类文件中,必须在main.m中明确导入。否则,编译器无法确定BNRItem类是否存在并会提示错误。

接下来创建一个新的BNRItem对象,然后将该对象的实例变量输出至控制台,代码如下:

int main (int argc, const char * argv)

{

@autoreleasepool {

NSMutableArray *items = [[NSMutableArray alloc] init];

[items addObject:@"One"];

[items addObject:@"Two"];

[items addObject:@"Three"];

[items insertObject:@"Zero" atIndex:0];

// 遍历items数组中的每一个item

for (NSString *item in items) {

// 打印对象信息

NSLog(@"%@", item);

}

BNRItem *item = [[BNRItem alloc] init];

NSLog(@"%@ %@ %@ %d", [item itemName], [item dateCreated],

[item serialNumber], [item valueInDollars]);

items = nil; 

}

return 0; 

}

构建并运行应用。在输出的末端,可以发现一行包含三个(null)和一个0的字符串。粗体代码首先创建了一个新的BNRItem对象,而输出结果就是该对象的实例变量的值(见图2-14)。

图2-14  实例变量的值

当某个对象被创建出来后,其所有的实例变量都会被设为默认值:如果实例变量是指向对象的指针,那么相应的指针会指向nil。如果实例变量是int这样的基本类型,那么其数值会是0。

要为新创建的BNRItem对象设置更有意义的数据,需要创建一些新对象,然后将这些对象作为实参传给该对象的存方法。

在main.m中加入以下代码:

// 请读者注意,这里省略了部分相邻代码

...

BNRItem *item = [[BNRItem alloc] init];

// 创建一个新的NSString对象"Red Sofa",并传给BNRItem对象

[item setItemName:@"Red Sofa"];

// 创建一个新的NSString对象"A1B2C",并传给BNRItem对象

[item setSerialNumber:@"A1B2C"];

// 将数值100传给BNRItem对象,赋给valueInDollars

[item setValueInDollars:100];

NSLog(@"%@ %@ %@ %d", [item itemName], [item dateCreated],

[item serialNumber], [item valueInDollars]);

...

构建并运行应用,能在控制台看到所有的实例变量输出(见图2-15),但是_dateCreated的值仍然是(null)。后面的章节中会学习如何在创建BNRItem对象时给_dateCreated赋值。

图2-15  给实例变量赋值

使用点语法

之前的代码是通过发送消息来存取实例变量的:

BNRItem *item = [[BNRItem alloc] init];

// 发送消息为_valueInDollars实例变量赋值

[item setValueInDollars:5];

// 发送消息获取_valueInDollars的值

int value = [item valueInDollars];

另一种方法是使用点语法(dot syntax),也叫做点符号(dot notation)。以下代码使用点语法存取实例变量:

BNRItem *item = [[BNRItem alloc] init];

// 使用点语法为_valueInDollars实例变量赋值

item.valueInDollars = 5;

// 使用点语法获取_valueInDollars的值

int value = item.valueInDollars;

语法格式为:消息接受者(item)后面加上一个“。”,再加上实例变量的名字(去掉变量名之前的下画线,如_valueInDollars改为valueInDollars)。

请注意,点语法在存和取方法中的用法相同(item.valueInDollars)。区别是:如果点语法用在赋值号左边,就表示存方法,用在右边则代表取方法。

点语法和存取方法在应用运行时没有区别。两种语法编译后的代码也一样,无论点是语法还是存取方法都会调用之前实现的valueInDollars和setValueInDollars:方法。

相对于调用存取方法,越来越多的Objective-C程序员更倾向于使用点语法,点语法的可读性更好,特别是在有多层嵌套消息的情况下。Apple的官方代码坚持使用点语法存取实例变量,因此本书也会这样做。

在main.m中修改代码,使用点语法存取实例变量:

...

BNRItem *item = [[BNRItem alloc] init];

// 创建一个新的NSString对象"Red Sofa",并传给BNRItem对象

[item setItemName:@"Red Sofa"];

item.itemName = @"Red Sofa";

// 创建一个新的NSString对象"A1B2C",并传给BNRItem对象

[item setSerialNumber:@"A1B2C"];

item.serialNumber = @"A1B2C";

// 将数值100传给BNRItem对象,赋给valueInDollars

[item setValueInDollars:100];

item.valueInDollars = 100;

NSLog(@"%@ %@ %@ %d", [item itemName], [item dateCreated],

[item serialNumber], [item valueInDollars]);

NSLog(@"%@ %@ %@ %d", item.itemName, item.dateCreated,

item.serialNumber, item.valueInDollars);

...

类方法和实例方法

Objective-C中的方法分为实例方法和类方法两种。类方法(class method)的作用通常是创建对象,或者获取类的某些全局属性。类方法不会作用在对象上,也不能存取实例变量。实例方法(instance method)则用来操作类的对象(对象有时也称为类的一个实例),例如,存取方法都是实例方法,用来设置和获取对象的实例变量。

调用实例方法时,需要向类的对象发送消息,而调用类方法时,则向类自身发送消息。

例如,在创建一个BNRItem对象时,首先向BNRItem类发送alloc(类方法)消息,然后向使用alloc方法创建的对象发送init(实例方法)消息。

前文介绍过的description即是一个实例方法。在下一节中,读者将为BNRItem实现description方法,返回一个描述BNRItem对象的字符串。在后面的章节中还将实现一个类方法,用来创建有随机数据的BNRItem对象。

覆盖方法

子类可以覆盖(override)父类的方法。以description为例,向某个NSObject对象发送description消息时,可以得到一个NSString对象。这个NSString对象会包含当前对象的类名和其在内存中的地址信息,例如:

<BNRQuizViewController:0x4b222a0>

任何一个NSObject的子类都可以覆盖description方法,使返回的字符串能更好地描述子类的对象。例如,NSString覆盖description,以返回NSString对象自身。NSArray覆盖description,以返回数组对象所包含的所有对象的描述字符串。

因为BNRItem是NSObject的子类(NSObject是最初声明description方法的类),所以在BNRItem类中重新实现description方法,就是在覆盖NSObject的description方法。

在BNRItem.m中要覆盖description方法,可以将新的代码写在@implementation和@end之间的任意位置(除了现有方法的花括号内)中实现,代码如下:

- (NSString *)description

{

NSString *descriptionString =

[[NSString alloc] initWithFormat:@"%@ (%@): Worth $%d, recorded on %@",

 self.itemName,

 self.serialNumber,

 self.valueInDollars,

 self.dateCreated];

return descriptionString;

}

请注意代码中没有直接传入实例变量的名称(例如_itemName),而是调用了存取方法(使用点语法)。使用存取方法访问实例变量是良好的编程习惯,即使是访问对象自身的实例变量,也应该使用存取方法。访问实例变量时,如果使用了存取方法,系统可以在操作实例变量时附加一些额外操作,后面的章节中会介绍到。

覆盖description方法后,向某个BNRItem对象发送description消息就会得到一个NSString对象,相对于内存地址,该字符串可以更好地描述BNRItem对象。

在main.m中删除之前通过NSLog输出BNRItem对象的实例变量的那行代码,改用覆盖后的description方法,代码如下:

...

item.valueInDollars = 100;

NSLog(@"%@ %@ %@ %d", item.itemName, item.dateCreated,

item.serialNumber, item.valueInDollars);

// 程序会先调用相应实参的description方法,

// 然后用返回的字符串替换%@

NSLog(@"%@", item);

items = nil;

构建并运行应用,在控制台中查看输出结果(见图2-16)。

图2-16  打印BNRItem对象的描述字符串

如果不是要覆盖父类的方法,而是要创建全新的实例方法,又该怎样做?要创建新的实例方法,需要先在相应的头文件中声明新的方法,然后在对应的实现文件中定义该方法。下面开始创建两个新的实例方法,用于初始化BNRItem对象。

初始化方法

BNRItem类目前还只能使用从NSObject类继承而来的init方法初始化对象。本节将创建两个新的实例方法用于初始化BNRItem对象,这种用于初始化类的对象的方法称为初始化方法(initialization method,或initializer)。

在BNRItem.h中声明两个初始化方法,代码如下:

NSDate *_dateCreated;

}

- (instancetype)initWithItemName:(NSString *)name

valueInDollars:(int)value

  serialNumber:(NSString *)sNumber;

- (instancetype)initWithItemName:(NSString *)name;

- (void)setItemName:(NSString *)str;

(稍后会学习instancetype。)

每个初始化方法的方法名都会以英文单词init开头。初始化方法的这种命名模式只是一种约定,不会使其有别于其他实例方法。但是,Objective-C中的命名约定很重要,应该严格遵守(这点特别重要,忽视Objective-C的命名约定会产生问题,其严重程度将远超大部分初学者的预期)。

初始化方法类似init,但是会带参数,用于初始化当前的对象。为了应对各种不同的初始化需要,很多类会提供一种以上的初始化方法。例如,第一个初始化方法有三个参数,分别用来设置BNRItem对象的名称、价值和序列号——必须知道这些信息才能使用该初始化方法。如果只知道BNRItem对象的名称,就使用第二个初始化方法。

指定初始化方法

任何一个类,无论有多少个初始化方法,都必须选定其中的一个作为指定初始化(designated initializer)方法。指定初始化方法要确保对象的每一个实例变量都处在一个有效的状态。有效(valid)一词有很多不同的意思,这里是指向初始化后的对象发送消息时,输出结果是可预期的,并且不会有“坏事”发生。

指定初始化方法的参数通常会和最重要的、最常用的实例变量相对应。以BNRItem类为例,它有四个实例变量,但是只有其中三个是可写的,因此BNRItem类的指定初始化方法应该有三个实参,并为_dateCreated赋值。打开BNRItem.h,在指定初始化方法前添加注释:

NSDate *_dateCreated;

}

// BNRItem类的指定初始化方法

- (instancetype)initWithItemName:(NSString *)name

  valueInDollars:(int)value

serialNumber:(NSString *)sNumber;

- (instancetype)initWithItemName:(NSString *)name;

- (void)setItemName:(NSString *)str;

instancetype

两个初始化方法的返回类型都是instancetype。该关键字表示方法的返回类型和调用方法的对象类型相同。init方法的返回类型都声明为instancetype。

为什么不将返回类型声明为BNRItem *?问题在于,BNRItem的子类会继承其全部方法,其中包括初始化方法和其返回类型。如果BNRItem的子类对象收到该初始化消息,那么返回的会是什么类型的对象?答案是相应子类的对象,而不是BNRItem对象。读者可能会想:“这个问题容易解决,在子类中覆盖初始化方法并修改返回类型即可”。但是,在Objective-C中,一个对象不能同时拥有两个选择器相同、但是返回类型(或者参数类型)不同的方法。

为了避免这个问题,可以声明init方法的返回类型和调用方法的对象类型相同,这样就保证了对象初始化后仍然是正确的类型。

id

在Objective-C引入instancetype关键字之前,初始化方法的返回类型都是id(读音“eye-dee”)。id的定义是“指向任意对象的指针”(id和C语言的void* 类似)。使用Xcode创建类文件时,模板代码中仍然使用id作为初始化方法的返回类型,我们希望Apple能尽快更新模板代码。

instancetype只能用来表示方法返回类型,但是id还可以用来表示变量和方法参数的类型。如果程序运行时无法确定一个对象的类型,就可以将该对象声明为id。

id objectOfUnknownType;

可以使用id快速遍历存储不同类型对象的数组:

for (id item in items) {

NSLog(@"%@", item);

}

请注意,因为id的定义是“指向任意对象的指针”,所以不能在变量名或参数名前再加“*”。

实现BNRItem类的指定初始化方法

在BNRItem.m中为BNRItem类实现指定初始化方法,代码如下:

@implementation BNRItem

- (instancetype)initWithItemName:(NSString *)name

  valueInDollars:(int)value

  serialNumber:(NSString *)sNumber

{

// 调用父类的指定初始化方法

self = [super init];

// 父类的指定初始化方法是否成功创建了父类对象?

if (self) {

// 为实例变量设定初始值

_itemName = name;

_serialNumber = sNumber;

_valueInDollars = value;

// 设置_dateCreated的值为系统当前时间

_dateCreated = [[NSDate alloc] init];

}

// 返回初始化后的对象的新地址

return self;

}

在以上代码中,请注意实例变量_dateCreated的值被设置为指向NSDate对象的指针,它表示系统当前时间。

接下来请看方法实现中的第一行代码。当编写指定初始化方法时,首先要做的事情是通过super关键字,调用父类的指定初始化方法。最后要做的事情是通过self关键字,返回一个指针,指向成功初始化后的对象。因此,要理解初始化方法就要先了解self和super。

self

self存在于方法中,是一个隐式(implicit)局部变量。编写方法时不需要声明self,并且程序会自动为self赋值,指向收到消息的对象自身(大多数面向对象的语言也有这个概念,有些将其称为this,而不是self)。通常情况下,self会用来向对象自己发送消息,代码如下:

- (void)chickenDance

{

[self pretendHandsAreBeaks];

[self flapWings];

[self shakeTailFeathers];

}

初始化方法的最后一行代码必须返回初始化后的对象。这样,调用者才能将该对象赋给指针变量。

return self;

super

在覆盖父类的某个方法时,往往需要保留该方法在父类中的实现,然后在其基础上扩充子类的实现。为了能更方便地完成这项任务,Objective-C提供了一个名为super的关键字:

- (void)someMethod

{

[super someMethod];

[self doMoreStuff];

}

super是如何工作的?通常情况下,当某个对象收到消息时,系统会先从这个对象的类开始,查询和消息名相同的方法名。如果没找到,则会在这个对象的父类中继续查找。该查询过程会沿着继承路径向上,直到找到相应的方法名为止(如果直到层次结构的顶端也没能找到合适的方法,程序就会抛出异常)。

向super发消息,其实是向self发消息,但是要求系统在查找方法时跳过当前对象的类,从父类开始查询。以BNRItem的指定初始化方法为例,向super发送init消息会调用NSObject的init。

确认父类的初始化结果

在调用父类的指定初始化方法之后,代码检查父类的初始化结果。如果初始化方法不能正确完成对象的初始化,就会返回nil。因此,在调用父类的初始化方法后,应该先将得到的返回值赋给self变量,然后确认该变量是不是nil,如果不是nil,再进行下一步的初始化工作。

初始化方法中的实例变量

现在请注意初始化方法的核心代码:初始化实例变量。之前本书提到,不要直接访问实例变量,而是使用存取方法。但是在初始化方法中相反,应该直接访问实例变量。

初始化方法在执行时,无法确定新创建对象的实例变量是否已经处于正确设置。实例变量可能具有不正确的值,也可能没有被正确分配,如果这时调用存取方法访问实例变量,则很可能会引起错误。本书在初始化方法中直接访问实例变量,不调用存取方法。

一些非常优秀的Objective-C程序员坚持在初始化方法中也使用存取方法。他们认为,如果存取方法中包含对实例变量的不安全操作,那么应该将这些复杂的代码提取出来,放到其他方法中,存取方法应该保持简单,只用来设置和获取实例变量。两种说法都有道理,但是本书选择在初始化方法中直接访问实例变量。

其他初始化方法与初始化方法链

接下来实现BNRItem的第二个初始化方法。实现initWithItemName:方法时,不需要将指定初始化方法中的代码搬过来再重写一遍。它只需要调用指定初始化方法,将得到的实参作为_itemName传入,而其他实参则使用某个默认值传入。

在BNRItem.m中实现initWithItemName:方法:

- (instancetype)initWithItemName:(NSString *)name

{

return [self initWithItemName:name

  valueInDollars:0

serialNumber:@""];

}

BNRItem还有第三个初始化方法——init,继承自父类NSObject。如果向某个BNRItem对象发送init消息,程序就不会调用之前创建的指定初始化方法。因此必须覆盖BNRItem类的init方法,将其和指定初始化方法“串联”起来。

覆盖BNRItem.m中的init方法,调用initWithItemName:方法,并使用默认值设置BNRItem对象的名称,代码如下:

- (instancetype)init

{

return [self initWithItemName:@"Item"];

}

现在,如果给BNRItem对象发送init消息,则会调用initWithItemName:方法,传入默认名称。而initWithItemName:方法又会调用initWithItemName:valueInDollars: serialNumber:方法并传入默认值和序列号。

图2-17列出了BNRItem类的多个初始化方法之间的关系。其中白色为指定初始化方法,灰色为其他初始化方法。

图2-17  初始化方法链

串联(chain)使用初始化方法的机制可以减少错误,也更容易维护代码。在创建类时,需要先确定指定初始化方法,然后只在指定初始化方法中编写初始化的核心代码,其他初始化方法只需要调用指定初始化方法(直接或间接)并传入默认值即可。

下面为初始化方法总结若干简单的规则。

•类会继承父类所有的初始化方法,也可以为类加入任意数量的初始化方法。

•每个类都要选定一个指定初始化方法。

•在执行其他初始化工作之前,必须先用指定初始化方法调用父类的指定初始化方法(直接或间接)。

•其他初始化方法要调用指定初始化方法(直接或间接)。

•如果某个类所声明的指定初始化方法与其父类的不同,就必须覆盖父类的指定初始化方法并调用新的指定初始化方法(直接或间接)。

使用初始化方法

现在可以使用BNRItem的指定初始化方法设置实例变量。

在main.m中找到创建并初始化BNRItem对象,并为其实例变量赋值的那几行代码。删除这几行代码,替换成如下所示的一行代码(创建BNRItem对象,然后调用指定初始化方法设置实例变量)。

...

// 遍历items数组中的每一个item

for (NSString *item in items) {

// 打印对象信息

NSLog(@"%@", item);

}

BNRItem *item = [[BNRItem alloc] init];

item.itemName = @"Red Sofa";

item.serialNumber = @"A1B2C";

item.valueInDollars = 100;

BNRItem *item = [[BNRItem alloc] initWithItemName:@"Red Sofa"

valueInDollars:100

serialNumber:@"A1B2C"];

NSLog(@"%@", item);

...

构建并运行应用,控制台会输出BNRItem对象的描述信息,且相应的数据应该和传入指定初始化方法的实参一致。

接下来验证另外两个初始化方法是否能正常工作。在main.m中使用initWithItemName:和init各创建一个BNRItem对象。

...

BNRItem *item = [[BNRItem alloc] initWithItemName:@"Red Sofa"

  valueInDollars:100

serialNumber:@"A1B2C"];

NSLog(@"%@", item);

BNRItem *itemWithName = [[BNRItem alloc]initWithItemName:@"Blue Sofa"];

NSLog(@"%@", itemWithName);

BNRItem *itemWithNoName = [[BNRItem alloc] init];

NSLog(@"%@", itemWithNoName);

items = nil; 

}

return 0; 

}

构建并运行应用,在控制台中检查BNRItem的初始化方法链是否正常工作(见图2-18)。

图2-18  三个初始化方法的输出结果

还剩最后一项工作——编写一个类方法创建有随机数据的BNRItem对象。

类方法

从语法上看,类方法的声明和实例方法的声明不同,差别在于第一个字符。在返回类型的前面,实例方法使用的是字符-,而类方法使用的是字符+。

在BNRItem.h中声明一个类方法,用来创建有随机数据的BNRItem对象,代码如下:

@interface BNRItem : NSObject

{

NSString *_itemName;

NSString *_serialNumber;

int _valueInDollars;

NSDate *_dateCreated;

}

+ (instancetype)randomItem;

- (instancetype)initWithItemName:(NSString *)name

 valueInDollars:(int)value

serialNumber:(NSString *)sNumber;

注意头文件中的声明顺序。实例变量声明应该写在最前面,然后是类方法,接下来是初始化方法,最后是其他方法。这种排列顺序是一种约定,可以使头文件更容易阅读。

在BNRItem.m中实现randomItem方法,创建、配置并返回一个BNRItem对象(再次提醒读者,新加入的代码要写在@implementation和@end之间),代码如下:

+ (instancetype)randomItem

{

// 创建不可变数组对象,包含三个形容词

NSArray *randomAdjectiveList = @[@"Fluffy", @"Rusty", @"Shiny"];

// 创建不可变数组对象,包含三个名词

NSArray *randomNounList = @[@"Bear", @"Spork", @"Mac"];

// 根据数组对象所含对象的个数,得到随机索引

// 注意:运算符%是模运算符,运算后得到的是余数

// 因此adjectiveIndex是一个0到2(包括2)的随机数

NSInteger adjectiveIndex = arc4random % [randomAdjectiveList count];

NSInteger nounIndex = arc4random % [randomNounList count];

// 注意,类型为NSInteger的变量不是对象,

// NSInteger是一种针对unsigned long(无符号长整数)的类型定义

NSString *randomName = [NSString stringWithFormat:@"%@ %@",

[randomAdjectiveList objectAtIndex:adjectiveIndex],

[randomNounList objectAtIndex:nounIndex]];

int randomValue = arc4random % 100;

NSString *randomSerialNumber = [NSString stringWithFormat:@"%c%c%c%c%c",

 '0' + arc4random % 10,

 'A' + arc4random % 26,

 '0' + arc4random % 10,

 'A' + arc4random % 26,

 '0' + arc4random % 10];

BNRItem *newItem = [[self alloc] initWithItemName:randomName

valueInDollars:randomValue

serialNumber:randomSerialNumber];

return newItem;

}

首先,方法体的前两行代码创建了两个不可变数组,分别是randomAdjectiveList和randomNounList。请注意创建数组的语法——“@”符号后面加上一对方括号,数组中的对象写在方括号里,用逗号隔开。(以上两个数组中的对象全部是NSString对象。)这是一种创建NSArray对象的简洁语法。请注意,这种语法只能创建不可变数组,如果要使用可变数组,则不能使用这种语法。

创建形容词和名词数组之后,randomItem方法会根据一个随机的形容词和一个随机的名词创建出一个字符串。此外,还会创建出一个随机的整数和另一个根据随机数和随机字符创建的字符串。

最后,randomItem方法会创建一个BNRItem对象,调用新对象的指定初始化方法并输入之前随机创建的对象和整数。

randomItem方法还使用NSString的类方法stringWithFormat:。调用stringWithFormat:方法时,要将相应的消息直接发送给NSString类。stringWithFormat:方法会根据传入的参数返回相应的NSString对象。在Objective-C中,如果某个类方法的返回类型是这个类的对象(例如stringWithFormat:和randomPossession),就可以将这种类方法称为便捷方法(convenience method)。

这里要注意randomItem是如何使用self的。因为它是类方法,所以self是指BNRItem类自身而不是某个对象。在便捷方法中,应该使用self,而不是直接使用类的类名。这样,子类也能使用父类的便捷方法,不至于发生错误。以BNRItem为例,如果创建一个BNRItem类的子类BNRToxicWasteItem,就可以向这个子类发送randomItem消息:

测试BNRItem类

现在开始编写本章最终版本的RandomItems。打开main.m,只保留创建和销毁items数组的代码,删除其他代码。然后向数组中添加10个有随机内容的BNRItem对象,最后输出至控制台(见图2-19)。

图2-19  有随机内容的BNRItem对象

int main (int argc, const char * argv)

{

@autoreleasepool {

NSMutableArray *items = [[NSMutableArray alloc] init];

[items addObject:@"One"];

[items addObject:@"Two"];

[items addObject:@"Three"];

[items insertObject:@"Zero" atIndex:0];

// 遍历items数组中的每一个item

for (NSString *item in items) {

// 打印对象信息

NSLog(@"%@", item);

}

BNRItem *item = [[BNRItem alloc] initWithItemName:@"Red Sofa"

valueInDollars:100

serialNumber:@"A1B2C"];

NSLog(@"%@", item);

BNRItem *itemWithName = [[BNRItem alloc] initWithItemName:@"Blue Sofa"];

NSLog(@"%@", itemWithName);

BNRItem *itemWithNoName = [[BNRItem alloc] init];

NSLog(@"%@", itemWithNoName);

for (int i = 0; i < 10; i++) {

BNRItem *item = [BNRItem randomItem];

[items addObject:item];

}

for (BNRItem *item in items) {

NSLog(@"%@", item);

}

items = nil; 

 }

 return 0; 

}

请注意代码中第一条循环语句没有使用快速遍历语法,因为循环语句是在向数组中添加对象,而不是遍历。

构建并运行应用,检查控制台中的输出。