别搞混类别(Category)、拓展(Extension)

很多人都知道类别、分类的用法,但是对于一些细节就不是很清楚了,本文主要梳理下这3个概念的细节

类别(Category)

文件特征

  • 类别文件有2个,分别为 .h 和 .m
  • 命名为: “类名+类别名.h”和“类名+类别名.m”

文件内容格式

.h 文件格式

#import "类名.h"

@interface 类名 (类别名)
// 在此处声明方法
@end

.m 文件格式

#import "类名+类别名.h"

@implementation 类名 (类别名)
// 在此处实现声明的方法
@end

类别的作用

拓展当前类,为类添加方法

类别的局限性

  • 无法向现有的类添加实例变量(编译器报“instance variables may not be placed in categories”)。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性

  • 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名,Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。

  • 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)

Category 的使用和注意

  1. Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法(比如:UIViewCtroller、UITableView这些)的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如:用Category 重写了 UIViewCtroller 的方法 A,那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug,那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A,这样子类调用方法 A 时是调用的自己重写的方法 A,消除了父类 Category 中重写方法对自己的影响

  2. Category拓展方法按照有没有重写当前类中的方法,分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法

  3. Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类

  4. 子类会不会继承父类的 Category: Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category,相当于子类重写了继承自父类的方法

  5. Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类,当给 A 类添加一个 Category sayHello 方法,仅有A 类的子类才可以使用 sayHello 方法

拓展(Extension)

文件特征

  • 只存在一个文件
  • 命名方式:“类名_拓展名.h”
#import "类名.h"

@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end

拓展的作用

  1. 为类增加额外的属性、成员变量、方法声明

  2. 一般将类拓展直接写到当前类的 .m 文件中。不单独创建

  3. 一般私有的属性和方法写到类拓展中

  4. 和 Category 类似,但是小括号里面没有拓展的名字

拓展的局限性

  1. Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。

  2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错

  3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。

//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END

//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
    self.skills =  @"iOS && Web && Node && Hybrid";
    self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
    NSLog(@"%@",self.skillStacks);
}
@end

总结

  1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)

  2. 如果 Category 中声明了1个属性,那么 Category 只会生成 setter 和 getter 的声明,不会有实现

  3. Extension 也被成为匿名的 Category

  4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。

「小插曲」:为 Category 实现属性的 Setter 和 Getter

#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Student)


/**< 学号*/
@property (nonatomic, strong) NSString *studyNumber;

@end

NS_ASSUME_NONNULL_END


#import "Person+Student.h"
#import <objc/message.h>

@implementation Person (Student)

- (void)sayHi {
    NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}


/*
 * 传统的做法是在 setter 里面这样写
 _studyNumber = studyNumber;
 ARC 自动管理内存
 MRC
 [_studyNumber release];
 [studyNumber retain];
 _studyNumber = studyNumber;
 
 但是在 Category里面不会生成对应的实例变量,因此我们可以利用 Runtime 为我们的 category 关联属性的值
 setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
 getter:objc_getAssociatedObject(self, @selector(firstView));
 }
 */
- (void)setStudyNumber:(NSString *)studyNumber {
    objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
                             , OBJC_ASSOCIATION_RETAIN);
}

//@selector(studyNumber)
- (NSString *)studyNumber {
    return  objc_getAssociatedObject(self, @selector(studyNumber));
}

@end

说明: objc_setAssociatedObject 的第二个参数是const void * _Nonnull key 所以可以用 “studyNumber” 或者利用 @selector() 的特性返回的数据类型也满足,所以示例代码选用第二种方式

给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。

比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSURL (Title)

@property (nonatomic, copy) NSString *LR_title;

@end

NS_ASSUME_NONNULL_END

#import "NSURL+Title.h"
#import <objc/runtime.h>

@implementation NSURL (Title)

- (void)setLR_title:(NSString *)LR_title
{
    objc_setAssociatedObject(self, @selector(LR_title), LR_title
                             , OBJC_ASSOCIATION_RETAIN);
}

- (NSString *)LR_title
{
    return objc_getAssociatedObject(self, @selector(LR_title));
}

@end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值