关于分类(Category)和类扩展(Extension)的解惑
代码部分
为了理解和区分分类和拓展的区别,我们一共需要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)其实遵循了【装饰者】设计模式。
分类和类扩展的异同
- 一个类可以拥有多个不同的扩展,也可以拥有多个不同的分类。
- 扩展本质上是让私有方法暴露的一个特殊渠道。
- 扩展不能去实现基类没有的方法。(属性本身也是方法)
- 基类中有的,扩展可以使之对外暴露;基类中没有的,扩展无能为力。
- 分类本质上是为了解决不能用继承所解决的问题,可以看做是一中轻量级的继承方案。(继承还拥有父类所有的公共方法)
- 分类可以实现基类中没有的额外方法。
- 基类中有的,分类会覆写(覆盖)它;基类中没有的,分了可以声明并实现它。
误区解析:
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方法,不能进行赋值。