iOS应用开发最佳实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 创建一个Objective-C类

Objective-C 中,一个类通常包含两个部分:@interface 及@implementation 部分。在Objective-C 中,只要我们看到“@”,都可以把它看做是 Objective-C 对 C 语言的扩展,@interface 部分定义对象的数据成员及方法接口,通常对应的是.h 头文件。而@implementation部分则为代码提供方法的具体实现,通常对应的是.m类实现文件。

在Xcode 4.3以前,没错,版本没有写错,是在Xcode 4.3以前,不管是公有对象(通过@public 来标记)还是私有对象(通过@private 来标记)都在头文件里进行定义,而从Xcode 4.3以后,尽管Xcode也支持这样来编写代码,但更现代更好的方式是将公开的、可供其他类访问的变量和方法定义在.h头文件中,而将私有的、只供类内部访问的变量和方法定义在.m类实现文件中,后续会介绍到如何来实现。

下面的例子将通过创建Person类来说明在Objective-C中如何创建和实现类。

2.2.1 通过Xcode创建Person类

本节将创建一个Person类。可以把Person类的所有代码都写在main.m代码中,不过在编码中这显然不是一个好的方法。为方便代码的管理,我们会创建专门的Person类,通常将类的定义和类的实现分拆在两个文件中,.h头文件中包含类的定义的代码,.m类文件中包含类的实现的代码。通过Xcode来创建类,Xcode会简化生成过程,自动创建.h和.m文件。

首先按照之前介绍的方法在Xcode中创建一个Command Line Tool类型的项目,命名为PersonConsole。接下来在顶部的Xcode菜单条中选择File→New→ New File或者在键盘上同时按N两键,会出现如图2-7所示的窗口。选中Mac OS X下的Cocoa,在右侧的项目模板列表中选中Objective-C Class,单击“Next”按钮。

图2-7 添加类文件向导

在接下来的如图2-8所示的窗口中输入类名Person,在“Subclass of”对应的下拉列表中可以选择类的父类,这里保留默认选项 NSObject。NSObject 如同 Java 里的java.lang.Object根类,也如同C#里的System.Object一样,是所有类的根父类。

图2-8 输入类的基本信息

NSObject 根类提供了与运行环境交互的基本接口及诸多的类的基本行为的定义。在Java和.NET中,根类是必需的,在Objective-C中,原则上根类不是必需的。有兴趣的开发者可以尝试写一个自己的根类,但是这会耗费大量的工作,而且不一定好用。

单击“Next”按钮后选择类文件的存储路径,并确认保存,在左侧的项目文件导航中可以看到Xcode帮助自动创建了Person.h及Person.m两个文件,如图2-9所示。

图2-9 建立好的类文件

打开Person.h,修改代码如下,完成后通过快+S组合键保存修改。

        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

接着打开Person.m文件,修改代码如下并保存:

        1: #import "Person.h"
        2:
        3: @implementation Person
        4: -(void)setAge: (int)theAge
        5: {
        6:   age = theAge;
        7: }
        8:
        9: -(void)setGender: (char)theGender
        10: {
        11:  gender = theGender;
        12: }
        13: -(void)print
        14: {
        15:  NSLog(@"年龄: %d,性别:%@", age, gender == 'M' ? @"男性" : @"女性");
        16: }
        17:
        18: @end

到现在关于Person类的所有代码均已经完成,读者若看不懂没有关系,后面会有详细的解读。为了测试我们新建的Person类是否能够正常工作,打开main.m文件,添加一些测试代码如下:

        1: #import <Foundation/Foundation.h>
        2: #import "Person.h"
        3:
        4: int main (int argc, const char *argv[])
        5: {
        6:   Person *person = [Person new];
        7:   [person setAge: 25];
        8:   [person setGender: 'M'];
        9:   [person print];
        10:  return 0;
        11:}

至此所有代码都完成了,单击窗口左上角的Run按钮或者按下+R组合键运行程序,可以看到如图2-10所示的效果。

图2-10 运行效果

2.2.2 对Person类的解读

下面对前面的代码进行逐一解读,了解 Objective-C 中创建和定义类的细节。首先从Person.h头文件开始。

1.解读Person类头文件

代码顺利执行后,回过头来看看代码的具体构成。首先看Person.h头文件。类的头文件中定义了能够公开被其他类访问的类成员变量和方法,注意当没有通过@public 来修饰类的成员变量时,定义在头文件中的类变量的默认访问权限是受保护的,相当于通过@protected进行了修饰,只有类及其子类才具有访问相应的类成员变量的权限:

        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

第1行是一个导入语句,导入了Foundation框架。注意导入Foundation/Foundation.h使用的是尖括号“< >”,而之后导入Person.h头文件时使用的是引号“""”,在Objective-C开发中,通常对库函数的引用使用尖括号,而对自定义类及文件等的引用使用双引号。

第2行就是前面介绍到的@interface,这个符号告诉编译器从这里开始直到@end为止都是对类的定义。这里的类名为 Person,继承自 NSObject 根父类,Person 类定义了两个类成员,age 用来存储年龄信息,gender 用来存储性别信息,这两个类成员变量用大括号包含起来,编译器认为这两个括号之间的定义是类成员变量的定义。如果类没有成员变量,大括号之间允许为空,甚至可以完全去掉大括号。和C#和Java不同,Objective-C要求在括号内不能有对方法的定义,而是将其放在括号外。

        2: @interface Person : NSObject
        3: {
        4:   int age;
        5:   char gender;
        6:}

从第8行开始定义了三个方法,每个方法的第1个字母是一个减号“-”,减号后面的方法是实体方法,实体方法只有在类实例化后,也就是创建了基于类的对象之后,才能通过对象来调用。与之相对应的概念是类方法,类方法以加号“+”开头,不需要创建对象,直接通过类名就可以调用,这和C#及Java中静态方法的概念非常类似:

        8: -(void)setAge: (int)theAge;
        9: -(void)setGender: (char)theGender;
        10: -(void)print;

接下来需要注意的是 Objective-C 中方法的返回类型需要用圆括号包含起来,当编译器看到减号或者加号后面的圆括号时就知道是在对方法的返回值类型进行声明了。

返回值声明之后是方法名,如果方法不带参数,就可以像第10行的print方法一样以分号结尾直接结束方法的定义。如果方法需要接收参数,可以在方法名之后带冒号,冒号后面的部分是对参数的定义。参数的定义同样分成两个部分,前面带括号的部分声明了参数的类型,后面的部分是参数的名称,如果方法有超过一个的参数,则除了对每一个变量声明返回类型、变量名称外,还需要对每一个变量的用处进行说明。例如上面的例子中,如果希望将年龄、性别两个变量放在一个方法中进行设置,可以按照下面的代码进行方法声明:

        -(void)setAge: (int)theAge Gender: (char)theGender;

上面的声明中的“Gender:”部分称为“中缀符”,中缀符对于习惯Java、C#的开发者来说看起来有些奇怪,它的优点在于让代码更好理解,使代码更具可读性。如果一个方法有5个或者更多的参数,通过中缀符就可以直接了解到每个参数的用处而不用查看帮助文档了。总结类的定义方法如下:

        @interface 类名:父类名{
            类成员变量类型类成员变量1;
            类成员变量类型类成员变量2;
            …
        }
        -(返回值类型)方法名;
        +(返回值类型)方法名;
        -(返回值类型)方法名:(变量类型) 变量1名中缀符: (变量类型) 变量2名中缀符:(变量类型) 变量3…;
        …
        @end

2.解读Person类文件

接下来再看Person.m文件,这个文件是Person类的实现文件,具体代码参考如下:

        1: #import "Person.h"
        2:
        3: @implementation Person
        4: -(void)setAge: (int)theAge
        5: {
        6:   age = theAge;
        7: }
        8:
        9: -(void)setGender: (char)theGender
        10: {
        11:  gender = theGender;
        12: }
        13: -(void)print
        14: {
        15:  NSLog(@"年龄: %d,性别:%@", age, gender == 'M' ? @"男性" : @"女性");
        16: }
        17:
        18: @end

类的实现文件的第1行就通过#import指令引用了Person.h头文件,对自定义头文件的习惯引用方式与库函数的引用方式略有不同,如前面提到的通常使用双引号而不是尖括号。

        1: #import "Person.h"

接下来的@implementation告诉编译器,从这里开始到@end为止的所有代码都是类的具体实现代码,上面的类文件中是setAge:、setGender:、print方法的具体实现代码,开始编写代码后,例如输入“-(void)setG”,Xcode开发环境有智能提醒代码的剩余部分,按回车键,Xcode就会自动补足剩余的方法定义代码,通过这种方式可以避免输入错误,另外也可以直接从头文件中把方法的定义复制过来。setAge:方法和 setGender:方法比较简单明了,接收输入参数给类的成员变量age和gender赋值。

        4: -(void)setAge: (int)theAge
        5: {
        6:   age = theAge;
        7: }
        8:
        9: -(void)setGender: (char)theGender
        10: {
        11:  gender = theGender;
        12: }

print方法中使用了前面介绍过的NSLog方法来在控制台输出信息,%d的用法和C语言里面的printf是一样的,这里对应的是age变量。%@是Objective-C独有的,用来输出对象类型变量,这里对应的是包含gender变量的三元表达式,如果用户设置gender为‘M’,就输出“男性”,如果不是就输出“女性”,这里的@“男性”和@“女性”都是NSString类型的字符串。

在Objective-C语言中,也支持char数组来表示字符串,例如@“男性”是一个NSString类型的字符串,“男性”就是一个普通的C语言字符串,两者之间的差别就在于有无“@”符号,但是因为C语言字符串对多语言字符的处理问题,我们这里使用NSString类型字符串。如果需要输出C语言字符串,可以使用格式化指令“%s”。

通常通过NSString类来定义字符串,NSString类来自于Foundation库,就像我们定义的Person类一样,提供了一些字符串处理的便捷方法。对于字符串类,需要在双引号前加“@”符号来进行修饰。这也是NSLog方法的参数为什么以“@”开头的原因。

类的具体实现中重要的是理解@implementation和@end,理解了这两个符号,其他的内容没有不好理解的。总结类的实现部分代码如下:

          @implementation 类名
          -(返回值类型)方法名{
              方法定义
              …
          }
          +(返回值类型)方法名{
              方法定义
              …
          }
          -(返回值类型)方法名:(变量类型) 变量1名中缀符: (变量类型) 变量2名中缀符:(变量类型) 变量3…{
              方法定义
              …
          }
          …
          @end

3.解读main方法:类的实例化

在前面的代码中定义了Person类的接口,并进一步通过代码实现了类的公开方法,但是现在还不能使用类,要使用类,需要经过一个名为“类的实例化”的过程。在这个过程中,Objective-C运行时环境会以我们定义的类作为模板,在内存里为类分配空间,初始化类的成员变量,之后持有这块内存访问权限的就是我们称之为“对象”的东西,对象就是实例化了的类。参照main.m文件的源代码,来看看在Objective-C中是如何进行类的实例化的。

          1: #import <Foundation/Foundation.h>
          2: #import "Person.h"
          3:
          4: int main (int argc, const char *argv[])
          5: {
          6:   Person *person = [Person new];
          7:   [person setAge: 25];
          8:   [person setGender: 'M'];
          9:   [person print];
          10:  return 0;
          11:}

第1行和第2行代码引入了Foundation框架及Person类的接口定义文件。第4行是main 方法的开头,第 6 行代码就是类的实例化代码。接触过 Java、C#之类的编程语言的开发者可能会看着很纳闷,从来实例化的时候都是new关键字在类名前面的。开发者没有看花眼,在Objective-C中,new关键字确实放在类名的后面,new相当于是我们前面提到过的类方法,Person类从根类NSObject继承了这个方法。在Objective-C中还可以通过以下代码来实例化对象:

        Person *person = [[Person alloc] init];

Objective-C的这种实例化对象的方式更加形象化,首先通过alloc方法分配内存,接下来通过init方法初始化成员变量,alloc和init方法都是继承自根类NSObject。还需要留意,在 Objective-C 语言中所有的对象都是引用,因此在定义对象的时候要在对象名前面加上指针符号“*”。指针的概念来自于C语言,说明该变量存储的实际上是一个内存地址,从这个地址开始的一片内存区域才是真正存储对象的地方。

解释到这里,还有很多开发人员会觉得好奇,这个对象名还有方法名外面的中括号是什么意思?提到这个就不得不提Objective-C语言中的另外一个概念:消息(message)。消息是对象可以执行的操作,用于通知对象去做什么,例如[person setAge: 25]告诉Person对象将年龄设置为 25 岁,对象接收消息后,将查询对应的类,以查找正确的代码来运行。最基本的发送消息的格式定义如下:

        [对象或类名方法名:参数1 中缀符: 参数2⋯]

其中参数序列根据实际情况可以有也可以没有,初试 Objective-C 语言开发的开发者需要注意的是,在没有参数的情况下,方法名后面不需要带冒号,带冒号会引起编译器编译错误。

Objective-C的消息发送可以进行嵌套发送,可以理解成在第1个消息发送返回值的基础上再进行消息发送,就如同前面提到的例子向Person类发送了alloc消息返回一个对象,然后再向这个对象发送init方法来返回Person对象。嵌套调用的格式定义如下:

        …[[对象或类名方法名:参数1 中缀符: 参数2…] 方法名:参数1 中缀符: 参数2…]…

Objective-C在编译的过程中,编译器会去检查方法是否有效,如果无效会给一个警告,编译器不会阻止你去执行代码,但在运行时如果消息发送给对象后找不到相对应的方法,会发生运行时错误。

有意思的是,Objective-C中有一个可以指向任何对象的指针类型id,id是一种泛型,可以用于表示任何对象。对Person类的实例化也可以改成如下代码,注意由于id本身就是指针,因此在声明对象时不需要再在对象前面加上指针符号“*”:

        1: id person = [[Person alloc] init];

2.2.3 类的构造方法

通过Xcode早期的版本创建类的时候,在.m类文件中会自动生成下面的一段代码:

        1: - (id)init{
        2:   Self = [super init];
        3:   if(self){
        4:        //Initialization code here
        5:   }
        6:   return self;
        7: }

这里的init方法是Objective-C的默认初始化方法,调用new关键字创建对象时实际上会调用到此方法。如果需要在创建对象时对类成员变量进行初始化,可以在init方法中完成, init方法返回id类型。事实上,在Objective-C代码地我们会更多中看到使用init方法或者以init打头的方法来初始化对象,init方法初始化对象与new方法相比有很多优点:

➢ new方法是从NeXT时代延续下来的,还留在Objective-C里面是为了保持向后兼容。

➢ 以alloc-init的方式来初始化对象理解起来更直观;

➢ 最重要的是,new方法只能使用默认的init方法,如果希望定义自己的初始化方法,就没法在new方法里面使用了。

Objective-C和C#、Java不同的一个地方是Objective-C并不需要类的构造方法和类名一致,在 Objective-C 中可以自定义任何名称的构造方法即初始化方法。Objective-C 类的默认初始化方法是init方法,这里说的自定义的初始化方法即前面提到的以init打头的方法,自定义初始化方法并没有强制使用init打头,更多地是一种约定俗成,大家看到后也无须过于纠结,任何返回类型为id或者“类名*”的方法都可以是初始化方法。下面的例子为Person类创建一个新的初始化方法,首先在Person.h文件中,在@end之前为新的初始化方法添加接口定义:

        1: - (id) initWithAge: (int) theAge Gender: (char) theGender;

接下来在Person.m文件中添加方法的具体实现代码:

        1: - (id) initWithAge: (int) theAge Gender: (char) theGender
        2: {
        3:   self = [super init];
        4:   if(self){
        5:        [self setAge:theAge;
        6:        [selfsetGender:theGender];
        7:   }
        8:   return self;
        9: }

最后,在main.m中添加测试代码来试试我们自定义的新初始化方法:

        1: Person *person1 = [[Person alloc] initWithAge: 28 Gender: 'F'];
        2: [person1 print];

+R组合键运行的结果如图2-11所示。

图2-11 运行效果

上面的代码段中只需要理解initWithAge: Gender:方法实现代码,self和Java语言类似,指的是对象自身,super则是指父类,这里Person类的父类是NSObject。首先通过[super init]调用父类的初始化方法init来完成对象的初始化,接下来的if(self)相当于if(self != nil),判断父类的初始化是否顺利,nil是Objective-C里的空对象,相当于Java或者C#的NULL,加入这个判断以确保父类成功的初始化并返回了对象,之后再调用 Person 类的 setAge:Gender:方法来初始化Person对象的成员变量。

2.2.4 继承和多态

面向对象编程的很大一个特性是继承,比如我们开发了一个Person类,如果要开发一个新的Student类来为学校编写应用,Person类的很多成员变量如年龄和性别其实是Student类也会有的成员变量,相应的年龄还有性别设置方法也是 Student 类需要有的方法。如果从头到尾再在Student类里写一遍代码肯定可以,但没有重用到Person类的代码,降低了编写代码的效率。继承为开发者提供了一个很好的解决方案,定义 Student 类的时候让Student类继承Person类,Student类就自动具有了Person类的相关成员变量和方法。

通俗点说,这个关系非常类似于家族里的继承,爷爷的财产可以给父亲继承,父亲将爷爷的财产发扬光大,将更多的财产给儿子继承,一代代往下传。类的继承关系也是这样, Student类继承自Person类,再在Person类的基础上添加自己的新成员变量如班级、学校等及相应的方法。还可以进一步定义 UniversityStudent 大学生类继承自 Student 类,除了Person类及Student类有的成员变量、方法外,还有自己的成员变量如专业等信息。

图2-12所示是与图形处理相关的类的继承关系树状图,Square(正方形)类继承自Rectangle(长方形)类,Rectangle 类继承自 Shape(形状)类,Shape 类继承自 Graphic (图形)类,而Graphic类则直接继承自根类NSObject。需要注意的是,在Objective-C中,最多只能继承一个父类。

图2-12 图形处理类的继承关系

在Objective-C中实现继承非常容易,在下面的例子中我们通过构建Person类的子类Student类来学习和掌握如何在Xcode中建立子类。

为此,首先新建一个“Command Line Tool”类型的项目,项目名为Student。接着在Finder中找到前面Person Console项目的Person.h及Person.m文件,将这两个文件复制到Student项目中,复制完成后Student项目导航栏应如图2-13所示。

图2-13 Student项目导航栏一览

接着通过File→New→New File,或者在键盘上同时按+N组合键打开创建新文件向导窗口,选择Objective-C class模板,单击“下一步”按钮,输入类名——Student”,如父类的下拉列表中选不到Person类,可以手工输入Person,单击“下一步”按钮选择存储路径后创建Student类,如图2-14所示。

图2-14 输入Student类的信息

上面的步骤完成后打开Student.h文件会看到代码编辑窗口出现如图2-15所示的错误提示信息,单击红色叹号图标可以看到具体的错误信息。这是因为Student.h头文件还没有导入对Person.h头文件的引用。此外Student.h头文件还有一处需要修改,由于Xcode并不知道Person类的父类,因此也无法判断应该导入哪些默认库,这里提供的是默认的Cocoa库,将其修改为Foundation库,如图2-15所示。

图2-15 Student.h头文件错误提示

修改Student.h头文件如下:

        1: #import <Foundation/Foundation.h>
        2: #import "Person.h"
        3:
        4: @interface Student: Person
        5:{
        6:   NSString *grade;
        7:   NSString *schoolName;
        8:}
        9:
        10: -(void) setGrade: (NSString *) theGrade;
        11: -(void) setSchoolName: (NSString *) theSchoolName;
        12: -(id) initWithAge: (int)theAge Gender: (char)theGender Grade: (NSString
    *)theGrade SchoolName: (NSString *)theSchoolName;
        13: @end

不要担心如果继承Person类是否就不能继承来自NSObject根类的方法,Objective-C类的继承关系是累加的,Person类继承自NSObject类,Student类继承自Person类也就默认继承了NSObject类的所有成员变量和方法。

代码第6行和第7行定义了两个NSString类型成员变量,grade变量存储年级信息, schoolName 变量存储学校名称。第 10 行~第 12 行定义了 Student 类的三个公开方法, setGrade:和setSchoolName:方法为Student类的两个成员变量添加了设置方法,而第12行的方法则为Student类创建了初始化方法。

接下来修改Student.m文件以实现类的公开方法:

        1: #import "Student.h"
        2: @implementation Student
        3:
        4: -(void)setGrade: (NSString *)theGrade
        5: {
        6:   grade = theGrade;
        7: }
        8:
        9: -(void)setSchoolName: (NSString *)theSchoolName
        10:{
        11:  schoolName = theSchoolName;
        12:}
        13:
        14: -(id)initWithAge: (int)theAge Gender: (char)theGender Grade: (NSString
    *)theGrade SchoolName: (NSString *)theSchoolName
        15: {
        16:  self = [self initWithAge:theAge Gender:theGender];
        17:  if(self)
        18:  {
        19:       [self setGrade:theGrade];
        20:       [self setSchoolName:theSchoolName];
        21:  }
        22:  return self;
        23: }
        24:
        25: -(void)print
        26: {
        27:  NSLog(@"年龄:&d;性别:%@;学校:%@;年级:%@;", age, gender == 'M' ? @"男性": @"
    女性", schoolName, grade);
        28: }
        29:
        30: @end

上面的代码比较好读懂,第4行和第9行的两个方法是两个设置器方法,所谓“设置器”,指的是该方法的主要目的是为类的成员变量赋值。第14行定义了一个类的初始化方法,这个初始化方法通过用户输入的年龄、性别、年级及学校名来初始化学生对象。在这个方法中,第16行通过self关键字调用initWithAge:Gender:方法初始化年龄和性别,注意在Student类中并没有定义这个方法,但是因为Student类继承自Person类,也就自动继承了父类的该方法。接着第19、20行调用Student类自有的setGrade:和setSchoolName:方法来设置年级和学校信息。

接下来对于第 25 行的 print 方法,细心的开发者也许会好奇,为什么这个方法名和Person类的一模一样?这是因为面向对象语言中有一种技术叫做重写,子类可以重写父类具有同样方法签名的方法——这个讲法的意思是方法名完全相同,方法的输入参数也完全相同。第25行的方法正是Student类重写了父类Person类的print方法,以提供自定义的打印输出方法。

最后在main.m文件中添加测试代码来测试Student类的调用。

        1: #import <Foundation/Foundation.h>
        2: #import "Student.h"
        3:
        4: int main(int argc, const char *argv[])
        5: {
        6:   Student *student = [[Student alloc] initWithAge: 15 Gender: 'M' Grade: @"二年
    级" SchoolName: @"红星中学"];
        7:   [student print];
        8:   return 0;
        9: }
        10: @end

至此所有代码全部完成,但是按下+R组合键查看运行结果时却报了如图2-16所示的5处编译错误,这是为什么呢?仔细检查代码并没有问题呀?

图2-16 编译器错误提示

留意具体的错误信息可以看到类似于“Undefined symbols for architecture x86_64…-[Student print] in Student.o”这样的错误,意思是在编译器进行编译时Student类找不到print方法的定义,这里的问题主要出在当我们将Person类的两个文件复制到Student项目中来的时候,编译器并没有自动将Person类链接进来进行编译,而print方法实际上是在Person.h里面定义的,因此会报这样一些错误。

解决方法是首先在项目导航栏选中项目,然后在中间的项目布局栏中选择 Targets 下方的 Student,最后在右边的顶部三个选项中选择 Build Phases,留意到中间有一块叫做Compile Sources的区域,在这块区域中将Person类的具体类代码文件Person.m添加进来就可以了,如图2-17所示。

图2-17 添加链接源

具体的添加方法是单击图2-17所示的Compile Sources区域左下角的“+”号,在弹出的窗口(如图2-18所示)中选择Person.m文件后,单击“Add”按钮。

图2-18 添加Person.m类文件到链接源

完成后再次按下+R组合键查看运行结果就可以看到预期的输出结果了,如图2-19所示。

图2-19 Student类测试运行效果

面向对象的技术中还有一种特性叫做多态(Polymorphism)。多态性允许将子对象赋给父对象,并在运行时根据子对象的特性来进行运作。这种方式给了编程很大的灵活和机动性,举个例子,汽车(Car)类包含轮胎(Tire)类作为它的类成员变量,但是随着技术的进步,轮胎不断地出现新的类型导致开发商开发了 TireAdvancedType1、TireAdvancedType2 等新类型,那是否每一次推出新的轮胎类型,汽车类都要做相应的修改呢?有了多态,这一切变得不必要。我们在汽车类的轮胎设置器中虽然设置轮胎接收的是轮胎类型的变量,但只要新的轮胎类型都是继承自Tire类,就可以把TireAdvancedType1、TireAdvancedType2 类型的轮胎对象赋给 Care 类的 Tire 对象成员变量。可以利用上面的Student类来演示这个效果,大家预计下面的这段代码会有什么输出结果呢?

        1: Person *student = [[Student alloc] init];
        2: [student print];

当调用print方法时调用的是Person类的print方法还是Student类的print方法呢?我们按下+R组合键来查看运行结果,如图2-20所示。

图2-20 Student类测试多态运行效果

结果正如我们所期待的,调用的是Student类的print方法,这正是多态的特性,在运行的时候会根据对象的性质来进行动态的判断。

2.2.5 选择器(selector)

在 Objective-C 代码进行编译时,会根据方法的名字(包括参数列表)确定一个唯一的编号,这样在执行时 Objective-C 运行时环境会根据方法的编号来寻找方法进行执行。这个寻找的过程如同在一个 ID 列表里面选择相应的编号来进行执行一样,所以在Objective-C中,有一个新的概念叫做选择器(selector)。selector有两个意思,一是指方法名本身,二是指方法名背后的那个编号,这个编号在 Objective-C 中也有自己的类型,叫做SEL。

选择器并不仅仅是 Objective-C 里的一个概念,这种机制提供了一种在执行程序时动态执行方法的功能。我们在实际编程的过程中也会遇到这样的场景,在编码时并不能确定要执行的方法是什么,而是需要在运行时来动态决定。选择器就可以帮助实现这样的需求,可以通过给一个来自NSObject根类的方法performSelector传递SEL参数,让对象动态地执行给定的方法。

有两种方法可以获得一个方法的SEL变量值:

        1: SEL 变量名 = @selector(方法名字);
        2: SEL 变量名 = NSSelectorFromString(方法名字的字符串)

其中第1行是直接在程序中写上方法的名字,第2行是写上方法名字的字符串,稍后会给出代码帮助理解。另外,Objective-C也提供了从SEL变量取回变量名的方法:

        1: NSString *变量名 = NSStringFromSelector(SEL参数);

一旦获得了SEL变量,就可以通过下面的调用给对象发送消息:

        [对象 performSelector: SEL变量];
        [对象 performSelector: SEL变量 withObject:参数1 withObject:参数2…];

根据方法是否带参数可以选择不同的performSelector方法来执行,以下的两行代码是等价的:

        1: [student performSelector: @selector(setAge:) withObject: theAge];
        2: [student setAge: theAge];

selector选择器和id类型组合起来能够为动态编程提供非常多的便利:

        1: id helper = getReceiverObject();
        2: SEL method = getSelector();
        3: [helper performSelector: method];

由于发送消息是在执行时发生的,编译器在编译时并不知道传递的方法名是否正确,执行时方法名如果不存在会抛出运行时异常。为了避免这样的异常,可以在调用performSelector 之前执行检查,测试接收对象是否能够处理发送的消息,从而决定是否执行performSelector:

        1: if( [student respondsToSelector: @selector(setAge:)])
        2:   [student performSelector: @selector(setAge: ) withObject: theAge);

下面修改上面Student项目的main.m文件来测试一下新学到的选择器知识。打开main.m文件,修改main函数如下:

        1: int main(int argc, const char *argv[])
        2: {
        3:   Student *student = [[Student alloc] initWithAge: 18 Gender: 'F'];
        4:   SEL setGradAlias = @selector(setGrade:);
        5:   SEL setSchoolNameAlias = NSSelectorFromString(@"setSchoolName:");
        6:   [student performSelector:setGradeAlias withObject: @"三年级"];
        7:  [student performSelector:setSchoolNameAlias withObject:@"胜利告终"];
        8:   [student performSelector:@selector(print)];
        9: }

运行试试,看看效果是否如我们所期待呢(可以忽略编译器的警告信息),如图2-21所示。

图2-21 测试选择器的用法