OC 多态

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OC 多态相关的知识,希望对你有一定的参考价值。

OC的指针类型的变量有两个:一个是编译时类型,一个是运行时类型。如果编译时类型和运行时类型不一致,就可能出现所谓的多态。也可以说,使不同的类共享相同方法名称的能力称为多态。
示例的体现形式是父类指针指向子类对象,如:父类名 *p = [子类名 new];
示例:

//  FKBase.h

#import <Foundation/Foundation.h>

@interface FKBase : NSObject
-(void) base;
-(void) test;
@end
//  FKBase.m

#import "FKBase.h"

@implementation FKBase
-(void) base {
    NSLog(@"父类的普通base方法");
}
-(void) test {
    NSLog(@"父类的将被覆盖的test方法");
}
@end
//  FKSubclass.h

#import <Foundation/Foundation.h>
#import "FKBase.h"

NS_ASSUME_NONNULL_BEGIN

@interface FKSubclass : FKBase

-(void) sub;

@end
//  FKSubclass.m

#import "FKSubclass.h"

@implementation FKSubclass

-(void) test {
    NSLog(@"子类的覆盖父类的test方法");
}
-(void) sub {
    NSLog(@"子类的sub方法");
}

@end
//  main.m

#import <Foundation/Foundation.h>
#import "FKSubclass.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //下面编译时类型和运行时类型完全一样,因此不存在多态
        FKBase* bc = [[FKBase alloc] init];
        //下面两次调用将执行FKbase的方法
        [bc base];
        [bc test];
        //下面编译时类型和运行时类型完全一样,因此不存在多态
        FKSubclass* sc = [[FKSubclass alloc] init];
        //下面调用将执行从父类继承到的base方法
        [sc base];
        //下面调用将执行子类重写的test方法
        [sc test];
        //下面调用将执行子类定义的sub方法
        [sc sub];
        //下面编译时类型和运行时类型不一样,发生多态
        FKBase* ploymophicBc = [[FKSubclass alloc] init];
        //下面调用将执行从父类继承到的base方法
        [ploymophicBc base];
        //下面调用将执行子类重写的test方法
        [ploymophicBc test];
        //因为编译时的类型是FKBase
        //FKBase类没有提供sub方法,所以下面代码编译时会出错
        //[ploymophicBc sub];
        //可将任何类型的指针变量赋给id类型的变量
        id dyna = ploymophicBc;
        [dyna sub];
    }
    return 0;
}

第三个指针变量 ploymophicBc 比较特殊,它的编译时类型为 FKBase,而运行时类型是 FKSubclass,当调用该指针变量的 test 方法时,实际上执行 FKSubclass 类中覆盖后的 test 方法,这就可能出现多态。
子类是一种特殊的父类,因此,OC允许把一个子类对象直接赋值给一个父类指针变量,无须任何类型转换,或者称为向上转型,向上转型由系统自动完成。
当把一个子类对象直接赋值给父类指针变量时,例如上面的FKBase* ploymophicBc = [[FKSubclass alloc] init];,这个ploymophicBc变量的编译时类型是 FKBase,运行时类型是 FKSubclass,当运行时调用该指针变量方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就会出现:相同类型的变量调用同一个方法时呈现出多种不同的行为特征,这就是多态。
上面的[ploymophicBc sub];会在编译时引发错误。虽然ploymophicBc变量实际指向的对象确实包含 sub 方法,但因为它的编译时类型为 FKBase ,因此编译时无法调用 sub 方法。
这就是多态的限制:不能使用指向子类对象的父类指针来调用子类扩展方法(父类中没有的方法)。
指针变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行运行时类型所具有的方法。
为了解决编译时类型检查的问题,可以使用 id 类型,id类型的变量可被赋值为任意类型的对象或任意类型的指针变量,而且使用id类型的变量可以调用该变量实际所指对象的方法。

指针变量的强制类型转换

除了id类型的变量之外,指针变量只能调用其编译时类型的方法,而不能调用其运行时类型的方法,即使它实际所指对象确实包含该方法。如果需要让这个指针变量调用其运行时类型的方法,则必须强制类型转换成运行时类型,强制类型转换需要借助类型转换运算符
类型转换运算符是圆括号,用法为:

(type*) variable

类型转换运算符可以将一个基本类型变量转换为另一种类型。此外,这个类型转换运算运算符还可以将一个指针类型变量转换成其子类类型。
其实,这种强制类型转换只是改变了该指针变量的编译时类型,但该变量所指对象的实际类型并不会发生任何改变,如果程序不加判断地进行转换,那么转换出来的变量在调用方法时就会引发错误。
示例:

#import <Foundation/Foundation.h>

int main(int argc, const 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;
        //程序调用data的isEqualToData:方法。
        //由于data的编译时类型是NSDate,编译时没有问题
        //data实际指向对象是NSString,程序执行时会引发错误
        NSLog(@"%d", [date isEqualToDate: [NSDate date]]);
    }
    return 0;
}

obj指针变量实际指向的对象就是NSString,程序尝试将obj2指针变量强制转换为NSDate然后再赋值给NSDate类型的变量。由于obj2变量实际所指向的对象是NSString,因此,此时date指针变量的编译时类型虽然是NSDate,但它实际指向的对象依然是NSString,所以程序调用date变量的isEqualToDate: 方法是,虽然编译可以通过,但运行时会引发错误。

判断指针变量的实际类型

为了保证程序能正常运行,一般建议在强制转换之前先判断该对象是否为该类或其他子类的实例。判断指针变量实际指向的对象是否为某个类、某个子类的实例,可通过如下方法完成。

  • -(BOOL)isMemberOfClass:clazz:判断对象是否为clazz类的实例
  • -(BOOL)isKindOfClass:clazz:判断对象是否为clazz类或其子类的实例
  • +(BOOL)isSubclassOfClass:clazz:类方法,判断当前类是否为clazz的子类

isKindOfClass的示例:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //声明hello时使用NSObject类,则hello的编译时类型是NSObject
        //NSObject是所有类的父类,但hello变量的实际类型是NSString
        NSObject* hello = @"Hello";
        //使用isKindOfClass:判断该变量所指的对象是否为指定类及其子类的实例
        //返回YES
        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:方法的主要作用是,在执行强制类型转换之前,首先判断前一个对象是否为该类的实例,是否可以成功转换。

以上是关于OC 多态的主要内容,如果未能解决你的问题,请参考以下文章

OC 多态

转 OC温故:类的三大特性(封装,继承,多态)

oc22--多态

OC的封装继承和多态

IOS学习-02 OC--封装继承多态

OC学习6——面相对象的三大特性