多态
Objective-C指针类型的变量有两个:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时所使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。
(1)多态性
下面的程序定义了一个Base类,该类的接口部分代码如下:
#import <Foundation/Foundation.h>
@interface Base: NSObject
- (void) base;
- (void) test;
@end
上面的Base类的接口部分实现了两个方法,接下来为该类提供实现部分,实现部分负责实现接口部分定义的两个方法。实现部分代码如下:
#import <Foundation/Foundation.h>
#import "Base.h"
@implementation Base
- (void) base {
NSLog(@"这是父类的普通base方法");
}
- (void) test {
NSLog(@"这是父类的将被覆盖的test方法");
}
@end
接下来为Base类定义一个子类,该子类将会覆盖父类中定义的test方法,下面是子类SubClass的接口部分代码:
#import <Foundation/Foundation.h>
#import "Base.h"
@interface SubClass: Base
- (void) sub;
@end
上面代码中,我们为SubClass类接口部分新增定义了一个sub方法,接下来我们为SubClass类提供实现部分,实现部分需要实现sub方法和重写test方法,其实现部分代码如下:
#import <Foundation/Foundation.h>
#import "SubClass.h"
@implementation SubClass
//重写父类的test方法
- (void) test {
NSLog(@"这是子类覆盖父类的test方法");
}
- (void) sub {
NSLog(@"子类的sub方法");
}
@end
上面我们重写了父类的test方法,测试该子类的代码如下:
#import <Foundation/Foundation.h>
#import "SubClass.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
//下面的编译时类型和运行时类型完全一样,因此不从在多态
Base* x = [[Base alloc] init];
[x base];
[x test];
//下面的编译时类型和运行时类型完全一样,因此不从在多态
SubClass* y = [[SubClass alloc] init];
//下面的调用将执行从父类继承到的base方法
[y base];
//下面调用将执行子类重写的test方法
[y test];
//下面调用将会执行子类定义的sub方法
[y sub];
//下面编译时类型和运行时类型不一样,发生多态
Base* apple = [[SubClass alloc] init];
//下面调用将执行从父类继承到的base方法
[apple base];
//下面调用将执行子类重写的test方法
[apple test];
//因为apple的编译时类型是Base且Base类没有提供sub方法,所以下面所注释的那行代码在正常编译时会出错
//[apple sub];
//可以将任何类型的指针变量赋值给id类型的变量
id z = apple;
[z sub];
}
return 0;
}
上面程序的main()函数中显式创建了4个指针类型的变量,对于前两个指针变量x和y,它们的编译时类型和运行时类型完全相同,因此,调用它们的方法都是正常的方法,但第三个指针变量apple则比较特殊,它的编译时类型是Base,而运行时类型是SubClass,当调用该指针变量的test方法时(Base类中定义了这个方法,SubClass又重写了这个方法),实际上执行的是SubClass类中覆盖后的方法,这就可能出现多态,第四个变量是用id类型声名的变量。
上面main()函数中注释掉了[apple sub]; ,这行代码在编译时会引发错误。虽然apple变量实际指向的对象确实包含sub方法,但因为它的编译时类型为Base,编译时无法调用sub方法,所以会引发错误。
子类是一种特殊的父类,因此,Objective-C允许把一个子类对象直接赋值给一个父类指针对象,无需任何类型转换,或者被称为向上转型,向上转型由系统自动完成。
当把一个子类对象直接赋值给父类指针变量时,例如上面的Base* apple = [[SubClass alloc] init]; ,这个apple变量的编译时类型是Base,而运行时类型是SubClass,运行时调用该指针变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类的行为特征,这就可能出现:相同类型的变量调用同一个方法时呈现出多种不同的行为特征,这就是多态,例如:
Base* x = [[Base alloc] init];//定义一个父类对象
Base* apple = [[SubClass alloc] init];//将一个子类对象直接赋值给父类指针变量
上面程序中x,apple两个变量,它们的编译时类型都相同都是Base,但他们执行test方法时呈现出不同的行为特征,其中x调用test方法时方法行为表现出父类的行为特征,apple调用test方法时方法行为表现出子类的行为特征,这就是多态。
指针变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行其运行时类型所具有的方法。因此,编写 Objective-C 代码时,指针变量只能调用声明该变量时所用类中包含的方法。
例如:
NSOject* p = [[Person alloc] init];
代码定义一个变量 p,则这个 p 只能调用 NSObject 类的方法,而不能调用 Person 类里定义的方法。
为了解决编译时类型检查的问题,Objective-C提供了一个id 类型,id 类型的变量可被赋值为任意类型的对象或任意类型的指针变量,而且使用 id 类型的变量可以调用该变量实际所指对象的方法。比如上面例子程序,程序将apple指针变量赋值给id类型的变量,这样程序将可以调用SubClass类中定义的sub实例方法了。
(2)指针变量的强制类型转换
编写 Objective-C 程序时,除了id 类型的变量之外,指针变量只能调用其编译时类型的方法,而不能调用其运行时类型的方法,即使它实际所指对象确实包含该方法。如果需要让这个指针变量调用其运行时类型的方法,则必须强制类型转换成运行时类型,强制类型转换需要借助类型转换运算符。
类型转换运算符是圆括号,类型转换运算符的用法为∶
(type*)variable
这种用法可以将variable变量转换为一个type*类型的变量
类型转换运算符可以将一个基本类型变量转换为另一种类型,另外,这个类型转换运算符还可以将一个指针类型变量转换成其子类类型。
这种强制类型转换只是改变了该指针变量的编译时类型,但该变量所指向对象的实际类型并不会发生任何改变,如果程序不加判断地进行转换,那么转换出来的变量在调用方法时就有可能会引发错误。
下面是进行强制类型转换的程序,该程序说明了哪些情况可以进行类型转换,哪些情况将会引发错误:
#import <Foundation/Foundation.h>
#import "SubClass.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
NSObject* obj = @"Hello";
//由于obj变量所指向的对象时NSString对象,所以运行时也可以通过
NSString* objStr = (NSString*)obj;
NSLog(@"%@", objStr);
//定义一个obj2变量,编译时类型为NSObject,实际类型为NSString
NSObject* obj2 = @"iOS";
//尝试将obj2强制转换为NSDate,但程序只是定义了一个NSDate类型的指针,该指针与obj2指向同一个对象
NSDate* date = (NSDate*)obj2;
//程序尝试调用date的word:方法
//由于date的编译时类型是NSDate,因此编译时没有任何问题
//由于date实际指向的对象时NSString,因此程序执行时就会引发错误(因为date实际指向的对象NSString中没有word:方法)
NSLog(@"%d", [date word:[NSDate date]]);//这行代码运行时会报错
}
return 0;
}
当把子类对象赋值给父类指针变量时,被称为向上转型,这种转型总是可以成功的,这也从另一个侧面证实了子类是一种特殊的父类。这种转型只是表明这个指针变量的编译时类型是父类,但实际执行它的方法时,依然表现出子类对象的行为方式。但把一个父类对象赋值给子类指针变量时,就需要进行强制类型转换,而且还可能在运行时产生错误。
(3)判断指针变量的实际类型
为了保证程序能正常运行,一般建议在执行强制转换之前先判断该对象是否为该类或其子类的实例。判断指针变量实际指向的对象是否为某个类、某个子类的实例,可通过如下方法完成:
- (BOOL)isMemberOfClass:clazz: 判断该对象是否为clazz类的实例
- (BOOL)isKindOfClass:clazz: 判断该对象是否为clazz类或其子类的实例
+ (BOOL)isSubclassOfClass:clazz: 这个是类方法,用于判断当前类是否为clazz的子类
实际写的时候将上面格式中的- (BOOL)替换成要判断的对象名,将上面格式中的+ (BOOL)替换成要判断的类名即可
上面的前两个方法可使用任何对象作为调用者,接着向该方法传入任意类,该方法将会返回一个 BOOL 类型的值,用于表明该变量实际指向对象的类型。
下面程序写一下isKindOfClass:方法的用法作为例子:
#import <Foundation/Foundation.h>
#import "SubClass.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
//声明hello时使用NSObject 类,则hello的编译时类型是 NSObject
//NSObject是所有类的父类,但hello变量的实际类型时NSString
NSObject* hello = @"Hello";
//使用isKindOfClass∶判断该变量所指的对象是否为指定类及其子类的实例
NSLog(@"字符串是否是NSObject类的实例:%d", (hello isKindOfClass:[NSObject class]));
//返回YES
NSLog(@"字符串是否是NSString类的实例:%d", (hello isKindOfClass:[NSString class]));
//返回NO
NSLog(@"字符串是否是NSDate类的实例:%d", (hello isKindOfClass:[NSDate class]));
NSString* a = @"Hello";
//返回NO
NSLog(@"a是否是NSDate类的实例:%d", (a isKindOfClass:[NSDate class]));
}
return 0;
}
从上面的代码可以看出,使用 isKindOfClass∶方法可以非常方便地判断一个指针变量所指对象的实际类型。而 isKindOfClass∶方法的主要作用是,在执行强制类型转换之前,首先判断前一个对象是否是该类的实例,是否可以成功地转换,从而保证代码更加健壮。