关于分类(Category)和类扩展(Extension)的解惑

2020-06-09  本文已影响0人  Mi欧阳

代码部分

为了理解和区分分类和拓展的区别,我们一共需要5个文件:
基类:Person.h、Person.m
扩展:Person_Extension.h
分类:Person+Category.h、Person+Category.m

首先我们创建一个类(Person)

#import <Foundation/Foundation.h>

@interface Person : NSObject

//对外暴露的公共方法
-(void)eat;

@end
#import "Person.h"

//寄存于.m文件中的扩展
@interface Person ()

//在.m文件中的扩展会自动为其中的属性添加getter、setter方法
@property (nonatomic,assign)int happyInt;

//声明私有方法
-(void)sleep;

@end

@implementation Person

//在.h文件中对外暴露,公共方法
-(void)eat{
    self.happyInt += 10;
    NSLog(@"使用技能,吃");
}

//私有方法实现
-(void)sleep{
    self.happyInt -= 10;
    NSLog(@"使用技能,睡觉");
}

//方法实现,默认是一个私有方法
-(void)walk{
    NSLog(@"使用技能,行走");
}
@end

创建一个类扩展 Person_Extension.h
代码如下:

#import "Person.h"

@interface Person ()

//暴露Person中的happyInt,并使之成为 readonly
@property (nonatomic,assign,readonly)int happyInt;

//可调用,但编译无法通过,person中未实现
//单独创建的扩展,基类不会对其中的属性实现其getter或setter方法
//想让该属性能使用有两种方法:
//1.在person中创建同名属性sadInt;2.在person中手动实现对应getter、setter方法
@property (nonatomic,assign)int sadInt;

//在Person中存在对应私有方法的实现,可正常调用
-(void)walk;

//在Person中存在对应私有方法的实现,可正常调用
-(void)sleep;

//在Person中不存在对应私有方法的实现,编译通过,但无法使用
-(void)read;

@end

创建一个分类Person+Category
Person+Category.h 代码

#import "Person.h"

@interface Person (Category)

//Category中写可以重写同名属性,但实际上这中方式并不规范,不推荐
//Category原则上是不可以添加任何属性的
//@property (nonatomic,assign,readwrite)int happyInt;

//非要在Category中添加属性,需要用runtime在.m中实现getter和setter方法
@property (nonatomic,copy)NSString *lucklyStr;

//实现额外的方法(基类本身未实现的方法)
-(void)fly;


@end

Person+Category.m代码

#import "Person+Category.h"
#import "Person_Extension.h"
#import <objc/runtime.h>


static NSString *lucklyStrKey = @"lucklyStrKey";   //定义一个key值

@implementation Person (Category)

-(void)fly{
    //可以使用 readonly修饰 的 happyInt 的值
    if(self.happyInt > 50){
        NSLog(@"使用技能,飞行");
    }else{
        NSLog(@"使用技能,失败");
    }
}

//runtime手动实现lucklyStr属性对应的setter方法
- (void)setLucklyStr:(NSString *)lucklyStr{
    objc_setAssociatedObject(self, &lucklyStrKey, lucklyStr,OBJC_ASSOCIATION_COPY);
}

//runtime手动实现lucklyStr属性对应的getter方法
- (NSString *)lucklyStr {
    return objc_getAssociatedObject(self, &lucklyStrKey);
}



/*
 系统警告:
 Category is implementing a method which will also be implemented by its primary class
 实际调用会以该方法内部实现为准
 */
-(void)walk{
    NSLog(@"Category,使用技能,行走");
}

@end

然后在ViewController中使用Person对象


#include <objc/runtime.h>
#import "ViewController.h"
#import "Person.h"
//不导入无法使用happyInt属性、walk方法、sleep方法
#import "Person_Extension.h"
//不导入无法使用luckStr属性、fly方法
#import "Person+Category.h"
/*
 注:
 通过打印Person的属性和方法可知:
 即使不导入Person_Extension和Person+Category
 luckStr属性、fly方法也会出现在Peron对象中,推测是编译后即加入其中了
 */

@interface ViewController ()



@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    //公共方法
    [person eat];
    //由于readonly所以只能读不能写
    NSLog(@"当前的开心程度 %d",person.happyInt);
    //由Person+Category添加的额外属性
    person.lucklyStr = @"幸运数字:3";
    //由Person_Extension暴露,Person负责实现
    [person sleep];
    //由Person_Extension暴露,Person+Category覆写
    //注:如果你打印Person的方法列表,会发现它有两个walk方法
    [person walk];
    //由Person+Category暴露并实现
    [person fly];
    //会发生报错
    //person.sadInt = 10;
    //会发生报错
    //[person read];
    
    /*****     利用动态特性访问属性或方法     *****/
    //用kvc的方式对happyInt属性赋值,成功(KVC在iOS中就是个BUG)
    [person setValue:@100 forKey:@"happyInt"];
    //输出结果:100
    NSLog(@"当前的开心程度:%d",person.happyInt);
    //用performSelector方法调用 fly ,输出飞行成功
    //这种方法甚至不用#import "Person+Category.h"
    [person performSelector:NSSelectorFromString(@"fly")];
}

控制台输出结果

2020-06-09 13:00:32.405 Test2[3886:109041] 使用技能,吃
2020-06-09 13:00:32.405 Test2[3886:109041] 当前的开心程度 10
2020-06-09 13:00:32.406 Test2[3886:109041] 使用技能,睡觉
2020-06-09 13:00:32.406 Test2[3886:109041] Category,使用技能,行走
2020-06-09 13:00:32.406 Test2[3886:109041] 当前的开心程度:100
2020-06-09 13:00:32.406 Test2[3886:109041] 使用技能,飞行

上述的代码内容演示了iOS中扩展和分类的基础特性和用法。

分类和拓展的特性

扩展:

1.可以在扩展中声明私有属性。仅可声明,不可进行实现;(详见误区解析Q2)
2.扩展可以把基类(Person)的私有属性和方法进行暴露;
3.扩展可以改变基类中私有属性的修饰符。如能达到对外只读,而对内可修改。但如果对内是readonly,那么扩展中的修饰符就不能是readwhite;(详见误区解析Q3)

分类:

1.分类的意义在于解决一些不能用继承来处理的问题;
2.分类主要作用是增加基类(Person)中没有的方法;
3.分类的结构体中没有属性列表,只有方法列表。所以分类原则上无法添加属性,但通过runtime可以实现属性的添加,并会被编译器承认;
4.分类中有和基类同名的方法, 会优先调用分类中的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类;
5.分类在运行时与基类想合并;
6.分类中的属性和方法,均可以被KVC这种动态访问的方式所访问;
注:分类(Category)其实遵循了【装饰者】设计模式。

分类和类扩展的异同

  1. 一个类可以拥有多个不同的扩展,也可以拥有多个不同的分类。
  2. 扩展本质上是让私有方法暴露的一个特殊渠道。
  3. 扩展不能去实现基类没有的方法。(属性本身也是方法)
  4. 基类中有的,扩展可以使之对外暴露;基类中没有的,扩展无能为力。
  5. 分类本质上是为了解决不能用继承所解决的问题,可以看做是一中轻量级的继承方案。(继承还拥有父类所有的公共方法)
  6. 分类可以实现基类中没有的额外方法。
  7. 基类中有的,分类会覆写(覆盖)它;基类中没有的,分了可以声明并实现它。

误区解析:

Q1.是否可认为扩展(Extension)常常是一个匿名的分类(Category)?
A:不可以。
扩展只有隐藏和显示基类中私有属性和方法的作用,扩展没有能力开辟新属性和方法。
而分类,主要起到为基类增加方法这一作用,如有必要,也可以配合runtime增加属性。

Q2.扩展是否有创建(声明+实现)私有属性的功能?
A:没有。扩展只有声明私有属性的功能,没有实现私有属性getter、setter方法的功能。
在Person_Extension中的 sadInt 属性就只有声明,没有实现,所以不管是给其赋值还是想获取其值都会发生编译错误。
在Person.m中的扩展中声明 happyInt 会自动进行实现,是因为两点:一、OC是自上而下的进行编译的;二、.m文件中还有 @implementation关键字,它才是实现类对象所有属性的关键。

Q3.Person中的happyInt如果是readonly修饰符,那么在扩展中是否可以用readwrite?
A:不可以。如果基类中使用readonly,那么对于happyInt属性,编译器只会为其生成getter方法,而没有setter方法的实现。所以即使你在Person_Extension中用 readwrite 修饰符,happyInt依然没有setter方法,不能进行赋值。

上一篇 下一篇

猜你喜欢

热点阅读